diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/system/gonk | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/system/gonk')
164 files changed, 74269 insertions, 0 deletions
diff --git a/dom/system/gonk/AudioChannelManager.cpp b/dom/system/gonk/AudioChannelManager.cpp new file mode 100644 index 000000000..977715a29 --- /dev/null +++ b/dom/system/gonk/AudioChannelManager.cpp @@ -0,0 +1,181 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIDocument.h" +#include "nsIDOMClassInfo.h" +#include "nsIDOMEvent.h" +#include "nsIDOMEventListener.h" +#include "nsPIDOMWindow.h" +#include "nsIDocShell.h" +#include "nsIPermissionManager.h" +#include "nsIInterfaceRequestorUtils.h" +#include "AudioChannelManager.h" +#include "mozilla/dom/AudioChannelManagerBinding.h" +#include "mozilla/dom/nsBrowserElement.h" +#include "mozilla/Services.h" + +namespace mozilla { +namespace dom { +namespace system { + +NS_IMPL_QUERY_INTERFACE_INHERITED(AudioChannelManager, DOMEventTargetHelper, + nsIDOMEventListener) +NS_IMPL_ADDREF_INHERITED(AudioChannelManager, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(AudioChannelManager, DOMEventTargetHelper) + +AudioChannelManager::AudioChannelManager() + : mVolumeChannel(-1) +{ + hal::RegisterSwitchObserver(hal::SWITCH_HEADPHONES, this); +} + +AudioChannelManager::~AudioChannelManager() +{ + hal::UnregisterSwitchObserver(hal::SWITCH_HEADPHONES, this); + + nsCOMPtr<EventTarget> target = do_QueryInterface(GetOwner()); + NS_ENSURE_TRUE_VOID(target); + + target->RemoveSystemEventListener(NS_LITERAL_STRING("visibilitychange"), + this, + /* useCapture = */ true); +} + +void +AudioChannelManager::Init(nsPIDOMWindowInner* aWindow) +{ + BindToOwner(aWindow); + + nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(GetOwner()); + NS_ENSURE_TRUE_VOID(target); + + target->AddSystemEventListener(NS_LITERAL_STRING("visibilitychange"), + this, + /* useCapture = */ true, + /* wantsUntrusted = */ false); +} + +JSObject* +AudioChannelManager::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return AudioChannelManagerBinding::Wrap(aCx, this, aGivenProto); +} + +void +AudioChannelManager::Notify(const hal::SwitchEvent& aEvent) +{ + mState = Some(aEvent.status()); + + DispatchTrustedEvent(NS_LITERAL_STRING("headphoneschange")); +} + +bool +AudioChannelManager::SetVolumeControlChannel(const nsAString& aChannel) +{ + if (aChannel.EqualsASCII("publicnotification")) { + return false; + } + + AudioChannel newChannel = AudioChannelService::GetAudioChannel(aChannel); + + // Only normal channel doesn't need permission. + if (newChannel != AudioChannel::Normal) { + nsCOMPtr<nsIPermissionManager> permissionManager = + services::GetPermissionManager(); + if (!permissionManager) { + return false; + } + uint32_t perm = nsIPermissionManager::UNKNOWN_ACTION; + permissionManager->TestPermissionFromWindow(GetOwner(), + nsCString(NS_LITERAL_CSTRING("audio-channel-") + + NS_ConvertUTF16toUTF8(aChannel)).get(), &perm); + if (perm != nsIPermissionManager::ALLOW_ACTION) { + return false; + } + } + + if (mVolumeChannel == (int32_t)newChannel) { + return true; + } + + mVolumeChannel = (int32_t)newChannel; + + NotifyVolumeControlChannelChanged(); + return true; +} + +bool +AudioChannelManager::GetVolumeControlChannel(nsAString & aChannel) +{ + if (mVolumeChannel >= 0) { + AudioChannelService::GetAudioChannelString( + static_cast<AudioChannel>(mVolumeChannel), + aChannel); + } else { + aChannel.AssignASCII(""); + } + + return true; +} + +void +AudioChannelManager::NotifyVolumeControlChannelChanged() +{ + nsCOMPtr<nsIDocShell> docshell = do_GetInterface(GetOwner()); + NS_ENSURE_TRUE_VOID(docshell); + + bool isActive = false; + docshell->GetIsActive(&isActive); + + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + if (!service) { + return; + } + + if (isActive) { + service->SetDefaultVolumeControlChannel(mVolumeChannel, isActive); + } else { + service->SetDefaultVolumeControlChannel(-1, isActive); + } +} + +NS_IMETHODIMP +AudioChannelManager::HandleEvent(nsIDOMEvent* aEvent) +{ + nsAutoString type; + aEvent->GetType(type); + + if (type.EqualsLiteral("visibilitychange")) { + NotifyVolumeControlChannelChanged(); + } + return NS_OK; +} + +void +AudioChannelManager::GetAllowedAudioChannels( + nsTArray<RefPtr<BrowserElementAudioChannel>>& aAudioChannels, + ErrorResult& aRv) +{ + MOZ_ASSERT(aAudioChannels.IsEmpty()); + + // Only main process is supported. + if (XRE_GetProcessType() != GeckoProcessType_Default) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsCOMPtr<nsPIDOMWindowInner> window = GetOwner(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsBrowserElement::GenerateAllowedAudioChannels(window, nullptr, nullptr, + aAudioChannels, aRv); + NS_WARNING_ASSERTION(!aRv.Failed(), "GenerateAllowedAudioChannels failed"); +} + +} // namespace system +} // namespace dom +} // namespace mozilla diff --git a/dom/system/gonk/AudioChannelManager.h b/dom/system/gonk/AudioChannelManager.h new file mode 100644 index 000000000..a460651e7 --- /dev/null +++ b/dom/system/gonk/AudioChannelManager.h @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_system_AudioChannelManager_h +#define mozilla_dom_system_AudioChannelManager_h + +#include "mozilla/dom/BrowserElementAudioChannel.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Hal.h" +#include "mozilla/HalTypes.h" +#include "mozilla/Maybe.h" +#include "AudioChannelService.h" + +namespace mozilla { +namespace hal { +class SwitchEvent; +typedef Observer<SwitchEvent> SwitchObserver; +} // namespace hal + +namespace dom { +namespace system { + +class AudioChannelManager final + : public DOMEventTargetHelper + , public hal::SwitchObserver + , public nsIDOMEventListener +{ +public: + AudioChannelManager(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIDOMEVENTLISTENER + + void Notify(const hal::SwitchEvent& aEvent); + + void Init(nsPIDOMWindowInner* aWindow); + + /** + * WebIDL Interface + */ + + nsPIDOMWindowInner* GetParentObject() const + { + return GetOwner(); + } + + virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + bool Headphones() + { + // Bug 929139 - Remove the assert check for SWITCH_STATE_UNKNOWN. + // If any devices (ex: emulator) didn't have the corresponding sys node for + // headset switch state then GonkSwitch will report the unknown state. + // So it is possible to get unknown state here. + if (mState.isNothing()) { + mState = Some(hal::GetCurrentSwitchState(hal::SWITCH_HEADPHONES)); + } + return mState.value() != hal::SWITCH_STATE_OFF && + mState.value() != hal::SWITCH_STATE_UNKNOWN; + } + + bool SetVolumeControlChannel(const nsAString& aChannel); + + bool GetVolumeControlChannel(nsAString& aChannel); + + IMPL_EVENT_HANDLER(headphoneschange) + + void GetAllowedAudioChannels( + nsTArray<RefPtr<mozilla::dom::BrowserElementAudioChannel>>& aAudioChannels, + mozilla::ErrorResult& aRv); + +protected: + virtual ~AudioChannelManager(); + +private: + void NotifyVolumeControlChannelChanged(); + + Maybe<hal::SwitchState> mState; + int32_t mVolumeChannel; +}; + +} // namespace system +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_system_AudioChannelManager_h diff --git a/dom/system/gonk/AudioManager.cpp b/dom/system/gonk/AudioManager.cpp new file mode 100644 index 000000000..88dff13f7 --- /dev/null +++ b/dom/system/gonk/AudioManager.cpp @@ -0,0 +1,1412 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +#include <android/log.h> +#include <cutils/properties.h> +#include <binder/IServiceManager.h> + +#include "AudioChannelService.h" +#include "AudioManager.h" + +#include "nsIObserverService.h" +#include "nsISettingsService.h" +#include "nsPrintfCString.h" + +#include "mozilla/Hal.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/MozPromise.h" +#include "mozilla/dom/ScriptSettings.h" +#include "base/message_loop.h" + +#include "BluetoothCommon.h" +#include "BluetoothHfpManagerBase.h" + +#include "nsJSUtils.h" +#include "nsThreadUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsXULAppAPI.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" + +using namespace mozilla::dom; +using namespace mozilla::dom::gonk; +using namespace android; +using namespace mozilla; +using namespace mozilla::dom::bluetooth; + +#undef LOG +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "AudioManager" , ## args) + +#define HEADPHONES_STATUS_HEADSET u"headset" +#define HEADPHONES_STATUS_HEADPHONE u"headphone" +#define HEADPHONES_STATUS_OFF u"off" +#define HEADPHONES_STATUS_UNKNOWN u"unknown" +#define HEADPHONES_STATUS_CHANGED "headphones-status-changed" +#define MOZ_SETTINGS_CHANGE_ID "mozsettings-changed" +#define AUDIO_CHANNEL_PROCESS_CHANGED "audio-channel-process-changed" +#define AUDIO_POLICY_SERVICE_NAME "media.audio_policy" +#define SETTINGS_SERVICE "@mozilla.org/settingsService;1" + +// Refer AudioService.java from Android +static const uint32_t sMaxStreamVolumeTbl[AUDIO_STREAM_CNT] = { + 5, // voice call + 15, // system + 15, // ring + 15, // music + 15, // alarm + 15, // notification + 15, // BT SCO + 15, // enforced audible + 15, // DTMF + 15, // TTS +#if ANDROID_VERSION < 19 + 15, // FM +#endif +}; + +static const uint32_t sDefaultStreamVolumeTbl[AUDIO_STREAM_CNT] = { + 3, // voice call + 8, // system + 8, // ring + 8, // music + 8, // alarm + 8, // notification + 8, // BT SCO + 15, // enforced audible // XXX Handle as fixed maximum audio setting + 8, // DTMF + 8, // TTS +#if ANDROID_VERSION < 19 + 8, // FM +#endif +}; + +static const int32_t sStreamVolumeAliasTbl[AUDIO_STREAM_CNT] = { + AUDIO_STREAM_VOICE_CALL, // voice call + AUDIO_STREAM_NOTIFICATION, // system + AUDIO_STREAM_NOTIFICATION, // ring + AUDIO_STREAM_MUSIC, // music + AUDIO_STREAM_ALARM, // alarm + AUDIO_STREAM_NOTIFICATION, // notification + AUDIO_STREAM_BLUETOOTH_SCO, // BT SCO + AUDIO_STREAM_ENFORCED_AUDIBLE,// enforced audible + AUDIO_STREAM_DTMF, // DTMF + AUDIO_STREAM_TTS, // TTS +#if ANDROID_VERSION < 19 + AUDIO_STREAM_MUSIC, // FM +#endif +}; + +static const uint32_t sChannelStreamTbl[NUMBER_OF_AUDIO_CHANNELS] = { + AUDIO_STREAM_MUSIC, // AudioChannel::Normal + AUDIO_STREAM_MUSIC, // AudioChannel::Content + AUDIO_STREAM_NOTIFICATION, // AudioChannel::Notification + AUDIO_STREAM_ALARM, // AudioChannel::Alarm + AUDIO_STREAM_VOICE_CALL, // AudioChannel::Telephony + AUDIO_STREAM_RING, // AudioChannel::Ringer + AUDIO_STREAM_ENFORCED_AUDIBLE,// AudioChannel::Publicnotification + AUDIO_STREAM_SYSTEM, // AudioChannel::System +}; + + +struct AudioDeviceInfo { + /** The string the value maps to */ + const char* tag; + /** The enum value that maps to this string */ + uint32_t value; +}; + +// Mappings audio output devices to strings. +static const AudioDeviceInfo kAudioDeviceInfos[] = { + { "earpiece", AUDIO_DEVICE_OUT_EARPIECE }, + { "speaker", AUDIO_DEVICE_OUT_SPEAKER }, + { "wired_headset", AUDIO_DEVICE_OUT_WIRED_HEADSET }, + { "wired_headphone", AUDIO_DEVICE_OUT_WIRED_HEADPHONE }, + { "bt_scoheadset", AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET }, + { "bt_a2dp", AUDIO_DEVICE_OUT_BLUETOOTH_A2DP }, +}; + +static const int kBtSampleRate = 8000; + +typedef MozPromise<bool, const char*, true> VolumeInitPromise; + +namespace mozilla { +namespace dom { +namespace gonk { + +/** + * We have five sound volume settings from UX spec, + * You can see more informations in Bug1068219. + * (1) Media : music, video, FM ... + * (2) Notification : ringer, notification ... + * (3) Alarm : alarm + * (4) Telephony : GSM call, WebRTC call + * (5) Bluetooth SCO : SCO call + **/ +struct VolumeData { + const char* mChannelName; + int32_t mStreamType; +}; + +static const VolumeData gVolumeData[] = { + {"audio.volume.content", AUDIO_STREAM_MUSIC}, + {"audio.volume.notification", AUDIO_STREAM_NOTIFICATION}, + {"audio.volume.alarm", AUDIO_STREAM_ALARM}, + {"audio.volume.telephony", AUDIO_STREAM_VOICE_CALL}, + {"audio.volume.bt_sco", AUDIO_STREAM_BLUETOOTH_SCO} +}; + +class RunnableCallTask : public Runnable +{ +public: + explicit RunnableCallTask(nsIRunnable* aRunnable) + : mRunnable(aRunnable) {} + + NS_IMETHOD Run() override + { + return mRunnable->Run(); + } +protected: + nsCOMPtr<nsIRunnable> mRunnable; +}; + +nsCOMPtr<nsISettingsServiceLock> +GetSettingServiceLock() +{ + nsresult rv; + nsCOMPtr<nsISettingsService> service = do_GetService(SETTINGS_SERVICE, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsCOMPtr<nsISettingsServiceLock> lock; + rv = service->CreateLock(nullptr, getter_AddRefs(lock)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + return lock.forget(); +} + +#if ANDROID_VERSION >= 21 +class GonkAudioPortCallback : public AudioSystem::AudioPortCallback +{ +public: + virtual void onAudioPortListUpdate() + { + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction([]() { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<AudioManager> audioManager = AudioManager::GetInstance(); + NS_ENSURE_TRUE(audioManager.get(), ); + audioManager->UpdateCachedActiveDevicesForStreams(); + audioManager->MaybeUpdateVolumeSettingToDatabase(); + }); + NS_DispatchToMainThread(runnable); + } + virtual void onAudioPatchListUpdate() { } + virtual void onServiceDied() { } +}; +#endif + +void +AudioManager::HandleAudioFlingerDied() +{ + //Disable volume change notification + mIsVolumeInited = false; + + uint32_t attempt; + for (attempt = 0; attempt < 50; attempt++) { + if (defaultServiceManager()->checkService(String16(AUDIO_POLICY_SERVICE_NAME)) != 0) { + break; + } + LOG("AudioPolicyService is dead! attempt=%d", attempt); + usleep(1000 * 200); + } + + MOZ_RELEASE_ASSERT(attempt < 50); + + // Indicate to audio HAL that we start the reconfiguration phase after a media + // server crash + AudioSystem::setParameters(0, String8("restarting=true")); + + // Restore device connection states + SetAllDeviceConnectionStates(); + + // Restore call state +#if ANDROID_VERSION < 17 + AudioSystem::setPhoneState(mPhoneState); +#else + AudioSystem::setPhoneState(static_cast<audio_mode_t>(mPhoneState)); +#endif + + // Restore master volume + AudioSystem::setMasterVolume(1.0); + + // Restore stream volumes + for (uint32_t streamType = 0; streamType < AUDIO_STREAM_CNT; ++streamType) { + mStreamStates[streamType]->InitStreamVolume(); + mStreamStates[streamType]->RestoreVolumeIndexToAllDevices(); + } + + // Indicate the end of reconfiguration phase to audio HAL + AudioSystem::setParameters(0, String8("restarting=true")); + + // Enable volume change notification + mIsVolumeInited = true; + mAudioOutDevicesUpdated = 0; + MaybeUpdateVolumeSettingToDatabase(true); +} + +class VolumeInitCallback final : public nsISettingsServiceCallback +{ +public: + NS_DECL_ISUPPORTS + + VolumeInitCallback() + : mInitCounter(0) + { + mPromise = mPromiseHolder.Ensure(__func__); + } + + RefPtr<VolumeInitPromise> GetPromise() const + { + return mPromise; + } + + NS_IMETHOD Handle(const nsAString& aName, JS::Handle<JS::Value> aResult) + { + RefPtr<AudioManager> audioManager = AudioManager::GetInstance(); + MOZ_ASSERT(audioManager); + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + NS_ConvertASCIItoUTF16 volumeType(gVolumeData[idx].mChannelName); + if (StringBeginsWith(aName, volumeType)) { + uint32_t device = GetDeviceFromSettingName(aName); + MOZ_ASSERT(device != AUDIO_DEVICE_NONE); + if (aResult.isInt32()) { + int32_t stream = gVolumeData[idx].mStreamType; + uint32_t volIndex = aResult.toInt32(); + nsresult rv = audioManager->ValidateVolumeIndex(stream, volIndex); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPromiseHolder.Reject("Error : invalid volume index.", __func__); + return rv; + } + audioManager->SetStreamVolumeForDevice(stream, volIndex, device); + } + + if (++mInitCounter == MOZ_ARRAY_LENGTH(kAudioDeviceInfos) * MOZ_ARRAY_LENGTH(gVolumeData)) { + mPromiseHolder.Resolve(true, __func__); + } + return NS_OK; + } + } + mPromiseHolder.Reject("Error : unexpected audio init event.", __func__); + return NS_OK; + } + + NS_IMETHOD HandleError(const nsAString& aName) + { + mPromiseHolder.Reject(NS_ConvertUTF16toUTF8(aName).get(), __func__); + return NS_OK; + } + +protected: + ~VolumeInitCallback() {} + + uint32_t GetDeviceFromSettingName(const nsAString& aName) const + { + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(kAudioDeviceInfos); ++idx) { + NS_ConvertASCIItoUTF16 device(kAudioDeviceInfos[idx].tag); + if (StringEndsWith(aName, device)) { + return kAudioDeviceInfos[idx].value; + } + } + return AUDIO_DEVICE_NONE; + } + + RefPtr<VolumeInitPromise> mPromise; + MozPromiseHolder<VolumeInitPromise> mPromiseHolder; + uint32_t mInitCounter; +}; + +NS_IMPL_ISUPPORTS(VolumeInitCallback, nsISettingsServiceCallback) + +static void +BinderDeadCallback(status_t aErr) +{ + if (aErr != DEAD_OBJECT) { + return; + } + + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction([]() { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<AudioManager> audioManager = AudioManager::GetInstance(); + NS_ENSURE_TRUE(audioManager.get(), ); + audioManager->HandleAudioFlingerDied(); + }); + + NS_DispatchToMainThread(runnable); +} + +bool +AudioManager::IsFmOutConnected() +{ + return mConnectedDevices.Get(AUDIO_DEVICE_OUT_FM, nullptr); +} + +NS_IMPL_ISUPPORTS(AudioManager, nsIAudioManager, nsIObserver) + +void +AudioManager::AudioOutDeviceUpdated(uint32_t aDevice) +{ + MOZ_ASSERT(audio_is_output_device(aDevice)); + mAudioOutDevicesUpdated |= aDevice; +} + +void +AudioManager::UpdateHeadsetConnectionState(hal::SwitchState aState) +{ + bool headphoneConnected = mConnectedDevices.Get(AUDIO_DEVICE_OUT_WIRED_HEADPHONE, + nullptr); + bool headsetConnected = mConnectedDevices.Get(AUDIO_DEVICE_OUT_WIRED_HEADSET, + nullptr); + if (aState == hal::SWITCH_STATE_HEADSET) { + UpdateDeviceConnectionState(true, + AUDIO_DEVICE_OUT_WIRED_HEADSET, + NS_LITERAL_CSTRING("")); + } else if (aState == hal::SWITCH_STATE_HEADPHONE) { + UpdateDeviceConnectionState(true, + AUDIO_DEVICE_OUT_WIRED_HEADPHONE, + NS_LITERAL_CSTRING("")); + } else if (aState == hal::SWITCH_STATE_OFF) { + if (headsetConnected) { + UpdateDeviceConnectionState(false, + AUDIO_DEVICE_OUT_WIRED_HEADSET, + NS_LITERAL_CSTRING("")); + } + if (headphoneConnected) { + UpdateDeviceConnectionState(false, + AUDIO_DEVICE_OUT_WIRED_HEADPHONE, + NS_LITERAL_CSTRING("")); + } + } +} + +void +AudioManager::UpdateDeviceConnectionState(bool aIsConnected, uint32_t aDevice, const nsCString& aDeviceName) +{ +#if ANDROID_VERSION >= 15 + bool isConnected = mConnectedDevices.Get(aDevice, nullptr); + if (isConnected && !aIsConnected) { + AudioSystem::setDeviceConnectionState(static_cast<audio_devices_t>(aDevice), + AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE, + aDeviceName.get()); + mConnectedDevices.Remove(aDevice); + } else if(!isConnected && aIsConnected) { + AudioSystem::setDeviceConnectionState(static_cast<audio_devices_t>(aDevice), + AUDIO_POLICY_DEVICE_STATE_AVAILABLE, + aDeviceName.get()); + mConnectedDevices.Put(aDevice, aDeviceName); + } +#if ANDROID_VERSION < 21 + // Manually call it, since AudioPortCallback is not supported. + // Current volumes might be changed by updating active devices in android + // AudioPolicyManager. + MaybeUpdateVolumeSettingToDatabase(); +#endif +#else + NS_NOTREACHED("Doesn't support audio routing on GB version"); +#endif +} + +void +AudioManager::SetAllDeviceConnectionStates() +{ + for (auto iter = mConnectedDevices.Iter(); !iter.Done(); iter.Next()) { + const uint32_t& device = iter.Key(); + nsCString& deviceAddress = iter.Data(); + AudioSystem::setDeviceConnectionState(static_cast<audio_devices_t>(device), + AUDIO_POLICY_DEVICE_STATE_AVAILABLE, + deviceAddress.get()); + } +#if ANDROID_VERSION < 21 + // Manually call it, since AudioPortCallback is not supported. + // Current volumes might be changed by updating active devices in android + // AudioPolicyManager. + MaybeUpdateVolumeSettingToDatabase(true); +#endif +} + +void +AudioManager::HandleBluetoothStatusChanged(nsISupports* aSubject, + const char* aTopic, + const nsCString aAddress) +{ +#ifdef MOZ_B2G_BT + bool isConnected = false; + if (!strcmp(aTopic, BLUETOOTH_SCO_STATUS_CHANGED_ID)) { + BluetoothHfpManagerBase* hfp = + static_cast<BluetoothHfpManagerBase*>(aSubject); + isConnected = hfp->IsScoConnected(); + } else { + BluetoothProfileManagerBase* profile = + static_cast<BluetoothProfileManagerBase*>(aSubject); + isConnected = profile->IsConnected(); + } + + if (!strcmp(aTopic, BLUETOOTH_SCO_STATUS_CHANGED_ID)) { + if (isConnected) { + String8 cmd; + cmd.appendFormat("bt_samplerate=%d", kBtSampleRate); + AudioSystem::setParameters(0, cmd); + SetForceForUse(nsIAudioManager::USE_COMMUNICATION, nsIAudioManager::FORCE_BT_SCO); + } else { + int32_t force; + GetForceForUse(nsIAudioManager::USE_COMMUNICATION, &force); + if (force == nsIAudioManager::FORCE_BT_SCO) { + SetForceForUse(nsIAudioManager::USE_COMMUNICATION, nsIAudioManager::FORCE_NONE); + } + } + } else if (!strcmp(aTopic, BLUETOOTH_A2DP_STATUS_CHANGED_ID)) { + if (!isConnected && mA2dpSwitchDone) { + RefPtr<AudioManager> self = this; + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction([self, isConnected, aAddress]() { + if (self->mA2dpSwitchDone) { + return; + } + self->UpdateDeviceConnectionState(isConnected, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP, + aAddress); + + String8 cmd("bluetooth_enabled=false"); + AudioSystem::setParameters(0, cmd); + cmd.setTo("A2dpSuspended=true"); + AudioSystem::setParameters(0, cmd); + self->mA2dpSwitchDone = true; + }); + MessageLoop::current()->PostDelayedTask( + MakeAndAddRef<RunnableCallTask>(runnable), 1000); + + mA2dpSwitchDone = false; + } else { + UpdateDeviceConnectionState(isConnected, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP, + aAddress); + String8 cmd("bluetooth_enabled=true"); + AudioSystem::setParameters(0, cmd); + cmd.setTo("A2dpSuspended=false"); + AudioSystem::setParameters(0, cmd); + mA2dpSwitchDone = true; +#if ANDROID_VERSION >= 17 + if (AudioSystem::getForceUse(AUDIO_POLICY_FORCE_FOR_MEDIA) == AUDIO_POLICY_FORCE_NO_BT_A2DP) { + SetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, AUDIO_POLICY_FORCE_NONE); + } +#endif + } + mBluetoothA2dpEnabled = isConnected; + } else if (!strcmp(aTopic, BLUETOOTH_HFP_STATUS_CHANGED_ID)) { + UpdateDeviceConnectionState(isConnected, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET, + aAddress); + UpdateDeviceConnectionState(isConnected, + AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET, + aAddress); + } else if (!strcmp(aTopic, BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID)) { + String8 cmd; + BluetoothHfpManagerBase* hfp = + static_cast<BluetoothHfpManagerBase*>(aSubject); + if (hfp->IsNrecEnabled()) { + cmd.setTo("bt_headset_name=<unknown>;bt_headset_nrec=on"); + AudioSystem::setParameters(0, cmd); + } else { + cmd.setTo("bt_headset_name=<unknown>;bt_headset_nrec=off"); + AudioSystem::setParameters(0, cmd); + } + } +#endif +} + +void +AudioManager::HandleAudioChannelProcessChanged() +{ + // Note: If the user answers a VoIP call (e.g. WebRTC calls) during the + // telephony call (GSM/CDMA calls) the audio manager won't set the + // PHONE_STATE_IN_COMMUNICATION audio state. Once the telephony call finishes + // the RIL plumbing sets the PHONE_STATE_NORMAL audio state. This seems to be + // an issue for the VoIP call but it is not. Once the RIL plumbing sets the + // the PHONE_STATE_NORMAL audio state the AudioManager::mPhoneAudioAgent + // member will call the NotifyStoppedPlaying() method causing that this function will + // be called again and therefore the audio manager sets the + // PHONE_STATE_IN_COMMUNICATION audio state. + + if ((mPhoneState == PHONE_STATE_IN_CALL) || + (mPhoneState == PHONE_STATE_RINGTONE)) { + return; + } + + RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate(); + bool telephonyChannelIsActive = service && service->TelephonyChannelIsActive(); + telephonyChannelIsActive ? SetPhoneState(PHONE_STATE_IN_COMMUNICATION) : + SetPhoneState(PHONE_STATE_NORMAL); +} + +nsresult +AudioManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + if ((strcmp(aTopic, BLUETOOTH_SCO_STATUS_CHANGED_ID) == 0) || + (strcmp(aTopic, BLUETOOTH_HFP_STATUS_CHANGED_ID) == 0) || + (strcmp(aTopic, BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID) == 0) || + (strcmp(aTopic, BLUETOOTH_A2DP_STATUS_CHANGED_ID) == 0)) { + nsCString address = NS_ConvertUTF16toUTF8(nsDependentString(aData)); + if (address.IsEmpty()) { + NS_WARNING(nsPrintfCString("Invalid address of %s", aTopic).get()); + return NS_ERROR_FAILURE; + } + + HandleBluetoothStatusChanged(aSubject, aTopic, address); + return NS_OK; + } + + else if (!strcmp(aTopic, AUDIO_CHANNEL_PROCESS_CHANGED)) { + HandleAudioChannelProcessChanged(); + return NS_OK; + } + + // To process the volume control on each volume categories according to + // change of settings + else if (!strcmp(aTopic, MOZ_SETTINGS_CHANGE_ID)) { + RootedDictionary<dom::SettingChangeNotification> setting(RootingCx()); + if (!WrappedJSToDictionary(aSubject, setting)) { + return NS_OK; + } + if (!StringBeginsWith(setting.mKey, NS_LITERAL_STRING("audio.volume."))) { + return NS_OK; + } + if (!setting.mValue.isNumber()) { + return NS_OK; + } + + uint32_t volIndex = setting.mValue.toNumber(); + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + if (setting.mKey.EqualsASCII(gVolumeData[idx].mChannelName)) { + SetStreamVolumeIndex(gVolumeData[idx].mStreamType, volIndex); + return NS_OK; + } + } + } + + NS_WARNING("Unexpected topic in AudioManager"); + return NS_ERROR_FAILURE; +} + +static void +NotifyHeadphonesStatus(hal::SwitchState aState) +{ + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + if (aState == hal::SWITCH_STATE_HEADSET) { + obs->NotifyObservers(nullptr, HEADPHONES_STATUS_CHANGED, HEADPHONES_STATUS_HEADSET); + } else if (aState == hal::SWITCH_STATE_HEADPHONE) { + obs->NotifyObservers(nullptr, HEADPHONES_STATUS_CHANGED, HEADPHONES_STATUS_HEADPHONE); + } else if (aState == hal::SWITCH_STATE_OFF) { + obs->NotifyObservers(nullptr, HEADPHONES_STATUS_CHANGED, HEADPHONES_STATUS_OFF); + } else { + obs->NotifyObservers(nullptr, HEADPHONES_STATUS_CHANGED, HEADPHONES_STATUS_UNKNOWN); + } + } +} + +class HeadphoneSwitchObserver : public hal::SwitchObserver +{ +public: + void Notify(const hal::SwitchEvent& aEvent) { + RefPtr<AudioManager> audioManager = AudioManager::GetInstance(); + MOZ_ASSERT(audioManager); + audioManager->HandleHeadphoneSwitchEvent(aEvent); + } +}; + +void +AudioManager::HandleHeadphoneSwitchEvent(const hal::SwitchEvent& aEvent) +{ + NotifyHeadphonesStatus(aEvent.status()); + // When user pulled out the headset, a delay of routing here can avoid the leakage of audio from speaker. + if (aEvent.status() == hal::SWITCH_STATE_OFF && mSwitchDone) { + + RefPtr<AudioManager> self = this; + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction([self]() { + if (self->mSwitchDone) { + return; + } + self->UpdateHeadsetConnectionState(hal::SWITCH_STATE_OFF); + self->mSwitchDone = true; + }); + MessageLoop::current()->PostDelayedTask( + MakeAndAddRef<RunnableCallTask>(runnable), 1000); + mSwitchDone = false; + } else if (aEvent.status() != hal::SWITCH_STATE_OFF) { + UpdateHeadsetConnectionState(aEvent.status()); + mSwitchDone = true; + } + // Handle the coexistence of a2dp / headset device, latest one wins. +#if ANDROID_VERSION >= 17 + int32_t forceUse = 0; + GetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, &forceUse); + if (aEvent.status() != hal::SWITCH_STATE_OFF && mBluetoothA2dpEnabled) { + SetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, AUDIO_POLICY_FORCE_NO_BT_A2DP); + } else if (forceUse == AUDIO_POLICY_FORCE_NO_BT_A2DP) { + SetForceForUse(AUDIO_POLICY_FORCE_FOR_MEDIA, AUDIO_POLICY_FORCE_NONE); + } +#endif +} + +AudioManager::AudioManager() + : mPhoneState(PHONE_STATE_CURRENT) + , mIsVolumeInited(false) + , mAudioOutDevicesUpdated(0) + , mSwitchDone(true) +#if defined(MOZ_B2G_BT) || ANDROID_VERSION >= 17 + , mBluetoothA2dpEnabled(false) +#endif +#ifdef MOZ_B2G_BT + , mA2dpSwitchDone(true) +#endif + , mObserver(new HeadphoneSwitchObserver()) +{ + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(kAudioDeviceInfos); ++idx) { + mAudioDeviceTableIdMaps.Put(kAudioDeviceInfos[idx].value, idx); + } + + AudioSystem::setErrorCallback(BinderDeadCallback); +#if ANDROID_VERSION >= 21 + android::sp<GonkAudioPortCallback> callback = new GonkAudioPortCallback(); + AudioSystem::setAudioPortCallback(callback); +#endif + + // Create VolumeStreamStates + for (uint32_t loop = 0; loop < AUDIO_STREAM_CNT; ++loop) { + VolumeStreamState* streamState = + new VolumeStreamState(*this, static_cast<audio_stream_type_t>(loop)); + mStreamStates.AppendElement(streamState); + } + // Initialize stream volumes with default values + for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) { + uint32_t volIndex = sDefaultStreamVolumeTbl[streamType]; + SetStreamVolumeForDevice(streamType, volIndex, AUDIO_DEVICE_OUT_DEFAULT); + } + UpdateCachedActiveDevicesForStreams(); + + RegisterSwitchObserver(hal::SWITCH_HEADPHONES, mObserver); + // Initialize headhone/heaset status + UpdateHeadsetConnectionState(hal::GetCurrentSwitchState(hal::SWITCH_HEADPHONES)); + NotifyHeadphonesStatus(hal::GetCurrentSwitchState(hal::SWITCH_HEADPHONES)); + + // Get the initial volume index from settings DB during boot up. + InitVolumeFromDatabase(); + + // Gecko only control stream volume not master so set to default value + // directly. + AudioSystem::setMasterVolume(1.0); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(obs); + if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_SCO_STATUS_CHANGED_ID, false))) { + NS_WARNING("Failed to add bluetooth sco status changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_A2DP_STATUS_CHANGED_ID, false))) { + NS_WARNING("Failed to add bluetooth a2dp status changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_HFP_STATUS_CHANGED_ID, false))) { + NS_WARNING("Failed to add bluetooth hfp status changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID, false))) { + NS_WARNING("Failed to add bluetooth hfp NREC status changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, MOZ_SETTINGS_CHANGE_ID, false))) { + NS_WARNING("Failed to add mozsettings-changed observer!"); + } + if (NS_FAILED(obs->AddObserver(this, AUDIO_CHANNEL_PROCESS_CHANGED, false))) { + NS_WARNING("Failed to add audio-channel-process-changed observer!"); + } + +} + +AudioManager::~AudioManager() { + AudioSystem::setErrorCallback(nullptr); +#if ANDROID_VERSION >= 21 + AudioSystem::setAudioPortCallback(nullptr); +#endif + hal::UnregisterSwitchObserver(hal::SWITCH_HEADPHONES, mObserver); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(obs); + if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_SCO_STATUS_CHANGED_ID))) { + NS_WARNING("Failed to remove bluetooth sco status changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_A2DP_STATUS_CHANGED_ID))) { + NS_WARNING("Failed to remove bluetooth a2dp status changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_HFP_STATUS_CHANGED_ID))) { + NS_WARNING("Failed to remove bluetooth hfp status changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, BLUETOOTH_HFP_NREC_STATUS_CHANGED_ID))) { + NS_WARNING("Failed to remove bluetooth hfp NREC status changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, MOZ_SETTINGS_CHANGE_ID))) { + NS_WARNING("Failed to remove mozsettings-changed observer!"); + } + if (NS_FAILED(obs->RemoveObserver(this, AUDIO_CHANNEL_PROCESS_CHANGED))) { + NS_WARNING("Failed to remove audio-channel-process-changed!"); + } +} + +static StaticRefPtr<AudioManager> sAudioManager; + +already_AddRefed<AudioManager> +AudioManager::GetInstance() +{ + // Avoid createing AudioManager from content process. + if (!XRE_IsParentProcess()) { + MOZ_CRASH("Non-chrome processes should not get here."); + } + + // Avoid createing multiple AudioManager instance inside main process. + if (!sAudioManager) { + sAudioManager = new AudioManager(); + ClearOnShutdown(&sAudioManager); + } + + RefPtr<AudioManager> audioMgr = sAudioManager.get(); + return audioMgr.forget(); +} + +NS_IMETHODIMP +AudioManager::GetMicrophoneMuted(bool* aMicrophoneMuted) +{ + + if (AudioSystem::isMicrophoneMuted(aMicrophoneMuted)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +AudioManager::SetMicrophoneMuted(bool aMicrophoneMuted) +{ + if (!AudioSystem::muteMicrophone(aMicrophoneMuted)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +AudioManager::GetPhoneState(int32_t* aState) +{ + *aState = mPhoneState; + return NS_OK; +} + +NS_IMETHODIMP +AudioManager::SetPhoneState(int32_t aState) +{ + if (mPhoneState == aState) { + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + nsString state; + state.AppendInt(aState); + obs->NotifyObservers(nullptr, "phone-state-changed", state.get()); + } + +#if ANDROID_VERSION < 17 + if (AudioSystem::setPhoneState(aState)) { +#else + if (AudioSystem::setPhoneState(static_cast<audio_mode_t>(aState))) { +#endif + return NS_ERROR_FAILURE; + } + +#if ANDROID_VERSION < 21 + // Manually call it, since AudioPortCallback is not supported. + // Current volumes might be changed by updating active devices in android + // AudioPolicyManager. + MaybeUpdateVolumeSettingToDatabase(); +#endif + mPhoneState = aState; + return NS_OK; +} + +NS_IMETHODIMP +AudioManager::SetForceForUse(int32_t aUsage, int32_t aForce) +{ +#if ANDROID_VERSION >= 15 + status_t status = AudioSystem::setForceUse( + (audio_policy_force_use_t)aUsage, + (audio_policy_forced_cfg_t)aForce); +#if ANDROID_VERSION < 21 + // Manually call it, since AudioPortCallback is not supported. + // Current volumes might be changed by updating active devices in android + // AudioPolicyManager. + MaybeUpdateVolumeSettingToDatabase(); +#endif + return status ? NS_ERROR_FAILURE : NS_OK; +#else + NS_NOTREACHED("Doesn't support force routing on GB version"); + return NS_ERROR_UNEXPECTED; +#endif +} + +NS_IMETHODIMP +AudioManager::GetForceForUse(int32_t aUsage, int32_t* aForce) { +#if ANDROID_VERSION >= 15 + *aForce = AudioSystem::getForceUse((audio_policy_force_use_t)aUsage); + return NS_OK; +#else + NS_NOTREACHED("Doesn't support force routing on GB version"); + return NS_ERROR_UNEXPECTED; +#endif +} + +NS_IMETHODIMP +AudioManager::SetAudioChannelVolume(uint32_t aChannel, uint32_t aIndex) +{ + if (aChannel >= NUMBER_OF_AUDIO_CHANNELS) { + return NS_ERROR_INVALID_ARG; + } + + return SetStreamVolumeIndex(sChannelStreamTbl[aChannel], aIndex); +} + +NS_IMETHODIMP +AudioManager::GetAudioChannelVolume(uint32_t aChannel, uint32_t* aIndex) +{ + if (aChannel >= NUMBER_OF_AUDIO_CHANNELS) { + return NS_ERROR_INVALID_ARG; + } + + if (!aIndex) { + return NS_ERROR_NULL_POINTER; + } + + return GetStreamVolumeIndex(sChannelStreamTbl[aChannel], aIndex); +} + +NS_IMETHODIMP +AudioManager::GetMaxAudioChannelVolume(uint32_t aChannel, uint32_t* aMaxIndex) +{ + if (aChannel >= NUMBER_OF_AUDIO_CHANNELS) { + return NS_ERROR_INVALID_ARG; + } + + if (!aMaxIndex) { + return NS_ERROR_NULL_POINTER; + } + + *aMaxIndex = mStreamStates[sChannelStreamTbl[aChannel]]->GetMaxIndex(); + return NS_OK; +} + +nsresult +AudioManager::ValidateVolumeIndex(int32_t aStream, uint32_t aIndex) const +{ + if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) { + return NS_ERROR_INVALID_ARG; + } + + uint32_t maxIndex = mStreamStates[aStream]->GetMaxIndex(); + if (aIndex > maxIndex) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult +AudioManager::SetStreamVolumeForDevice(int32_t aStream, + uint32_t aIndex, + uint32_t aDevice) +{ + if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) { + return NS_ERROR_INVALID_ARG; + } + + int32_t streamAlias = sStreamVolumeAliasTbl[aStream]; + VolumeStreamState* streamState = mStreamStates[streamAlias].get(); + return streamState->SetVolumeIndexToAliasStreams(aIndex, aDevice); +} + +nsresult +AudioManager::SetStreamVolumeIndex(int32_t aStream, uint32_t aIndex) +{ + if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) { + return NS_ERROR_INVALID_ARG; + } + + int32_t streamAlias = sStreamVolumeAliasTbl[aStream]; + + nsresult rv; + for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) { + if (streamAlias == sStreamVolumeAliasTbl[streamType]) { + rv = mStreamStates[streamType]->SetVolumeIndexToActiveDevices(aIndex); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + // AUDIO_STREAM_FM is not used on recent gonk. + // AUDIO_STREAM_MUSIC is used for FM radio volume control. +#if ANDROID_VERSION < 19 + if (streamAlias == AUDIO_STREAM_MUSIC && IsFmOutConnected()) { + rv = mStreamStates[AUDIO_STREAM_FM]-> + SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_FM); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } +#endif + + MaybeUpdateVolumeSettingToDatabase(); + return NS_OK; +} + +nsresult +AudioManager::GetStreamVolumeIndex(int32_t aStream, uint32_t *aIndex) +{ + if (!aIndex) { + return NS_ERROR_INVALID_ARG; + } + + if (aStream <= AUDIO_STREAM_DEFAULT || aStream >= AUDIO_STREAM_MAX) { + return NS_ERROR_INVALID_ARG; + } + + *aIndex = mStreamStates[aStream]->GetVolumeIndex(); + return NS_OK; +} + +nsAutoCString +AudioManager::AppendDeviceToVolumeSetting(const char* aName, uint32_t aDevice) +{ + nsAutoCString topic; + topic.Assign(aName); + topic.Append("."); + uint32_t index = 0; + DebugOnly<bool> exist = mAudioDeviceTableIdMaps.Get(aDevice, &index); + MOZ_ASSERT(exist); + topic.Append(kAudioDeviceInfos[index].tag); + return topic; +} + +void +AudioManager::InitVolumeFromDatabase() +{ + nsresult rv; + nsCOMPtr<nsISettingsService> service = do_GetService(SETTINGS_SERVICE, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsISettingsServiceLock> lock; + rv = service->CreateLock(nullptr, getter_AddRefs(lock)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RefPtr<VolumeInitCallback> callback = new VolumeInitCallback(); + MOZ_ASSERT(callback); + callback->GetPromise()->Then(AbstractThread::MainThread(), __func__, this, + &AudioManager::InitDeviceVolumeSucceeded, + &AudioManager::InitDeviceVolumeFailed); + + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + for (uint32_t idx2 = 0; idx2 < MOZ_ARRAY_LENGTH(kAudioDeviceInfos); ++idx2) { + lock->Get(AppendDeviceToVolumeSetting(gVolumeData[idx].mChannelName, + kAudioDeviceInfos[idx2].value).get(), + callback); + } + } +} + +void +AudioManager::InitDeviceVolumeSucceeded() +{ + mIsVolumeInited = true; + MaybeUpdateVolumeSettingToDatabase(true); +} + +void +AudioManager::InitDeviceVolumeFailed(const char* aError) +{ + // Default volume of AUDIO_DEVICE_OUT_DEFAULT is already set. + mIsVolumeInited = true; + MaybeUpdateVolumeSettingToDatabase(true); + NS_WARNING(aError); +} + +void +AudioManager::MaybeUpdateVolumeSettingToDatabase(bool aForce) +{ + if (!mIsVolumeInited) { + return; + } + + nsCOMPtr<nsISettingsServiceLock> lock = GetSettingServiceLock(); + if (NS_WARN_IF(!lock)) { + return; + } + + // Send events to update the Gaia volumes + JS::Rooted<JS::Value> value(RootingCx()); + uint32_t volume = 0; + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + int32_t streamType = gVolumeData[idx].mStreamType; + VolumeStreamState* streamState = mStreamStates[streamType].get(); + if(!aForce && !streamState->IsDevicesChanged()) { + continue; + } + // Get volume index of active device. + volume = streamState->GetVolumeIndex(); + value.setInt32(volume); + lock->Set(gVolumeData[idx].mChannelName, value, nullptr, nullptr); + } + + // For reducing the code dependency, Gaia doesn't need to know the + // device volume, it only need to care about different volume categories. + // However, we need to send the setting volume to the permanent database, + // so that we can store the volume setting even if the phone reboots. + + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + int32_t streamType = gVolumeData[idx].mStreamType; + VolumeStreamState* streamState = mStreamStates[streamType].get(); + + if(!streamState->IsVolumeIndexesChanged()) { + continue; + } + + uint32_t remainingDevices = mAudioOutDevicesUpdated; + for (uint32_t i = 0; remainingDevices != 0; i++) { + uint32_t device = (1 << i); + if ((device & remainingDevices) == 0) { + continue; + } + remainingDevices &= ~device; + if (!mAudioDeviceTableIdMaps.Get(device, nullptr)) { + continue; + } + volume = streamState->GetVolumeIndex(device); + value.setInt32(volume); + lock->Set(AppendDeviceToVolumeSetting(gVolumeData[idx].mChannelName, + device).get(), + value, nullptr, nullptr); + } + } + + // Clear changed flags + for (uint32_t idx = 0; idx < MOZ_ARRAY_LENGTH(gVolumeData); ++idx) { + int32_t streamType = gVolumeData[idx].mStreamType; + mStreamStates[streamType]->ClearDevicesChanged(); + mStreamStates[streamType]->ClearVolumeIndexesChanged(); + } + // Clear mAudioOutDevicesUpdated + mAudioOutDevicesUpdated = 0; +} + +void +AudioManager::UpdateCachedActiveDevicesForStreams() +{ + // This function updates cached active devices for streams. + // It is used for optimization of GetDevicesForStream() since L. + // AudioManager could know when active devices + // are changed in AudioPolicyManager by onAudioPortListUpdate(). + // Except it, AudioManager normally do not need to ask AuidoPolicyManager + // about current active devices of streams and could use cached values. + // Before L, onAudioPortListUpdate() does not exist and GetDevicesForStream() + // does not use the cache. Therefore this function do nothing. +#if ANDROID_VERSION >= 21 + for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) { + // Update cached active devices of stream + mStreamStates[streamType]->IsDevicesChanged(false /* aFromCache */); + } +#endif +} + +uint32_t +AudioManager::GetDevicesForStream(int32_t aStream, bool aFromCache) +{ +#if ANDROID_VERSION >= 21 + // Since Lollipop, devices update could be notified by AudioPortCallback. + // Cached values can be used if there is no update. + if (aFromCache) { + return mStreamStates[aStream]->GetLastDevices(); + } +#endif + +#if ANDROID_VERSION >= 17 + audio_devices_t devices = + AudioSystem::getDevicesForStream(static_cast<audio_stream_type_t>(aStream)); + + return static_cast<uint32_t>(devices); +#else + // Per audio out device volume is not supported. + // Use AUDIO_DEVICE_OUT_SPEAKER just to store audio volume to DB. + return AUDIO_DEVICE_OUT_SPEAKER; +#endif +} + +uint32_t +AudioManager::GetDeviceForStream(int32_t aStream) +{ + uint32_t devices = + GetDevicesForStream(static_cast<audio_stream_type_t>(aStream)); + uint32_t device = SelectDeviceFromDevices(devices); + return device; +} + +/* static */ uint32_t +AudioManager::SelectDeviceFromDevices(uint32_t aOutDevices) +{ + uint32_t device = aOutDevices; + + // See android AudioService.getDeviceForStream(). + // AudioPolicyManager expects it. + // See also android AudioPolicyManager::getDeviceForVolume(). + if ((device & (device - 1)) != 0) { + // Multiple device selection. + if ((device & AUDIO_DEVICE_OUT_SPEAKER) != 0) { + device = AUDIO_DEVICE_OUT_SPEAKER; +#if ANDROID_VERSION >= 21 + } else if ((device & AUDIO_DEVICE_OUT_HDMI_ARC) != 0) { + device = AUDIO_DEVICE_OUT_HDMI_ARC; + } else if ((device & AUDIO_DEVICE_OUT_SPDIF) != 0) { + device = AUDIO_DEVICE_OUT_SPDIF; + } else if ((device & AUDIO_DEVICE_OUT_AUX_LINE) != 0) { + device = AUDIO_DEVICE_OUT_AUX_LINE; +#endif + } else { + device &= AUDIO_DEVICE_OUT_ALL_A2DP; + } + } + MOZ_ASSERT(audio_is_output_device(device)); + return device; +} +AudioManager::VolumeStreamState::VolumeStreamState(AudioManager& aManager, + int32_t aStreamType) + : mManager(aManager) + , mStreamType(aStreamType) + , mLastDevices(0) + , mIsDevicesChanged(true) + , mIsVolumeIndexesChanged(true) +{ + InitStreamVolume(); +} + +bool +AudioManager::VolumeStreamState::IsDevicesChanged(bool aFromCache) +{ + uint32_t devices = mManager.GetDevicesForStream(mStreamType, aFromCache); + if (devices != mLastDevices) { + mLastDevices = devices; + mIsDevicesChanged = true; + } + return mIsDevicesChanged; +} + +void +AudioManager::VolumeStreamState::ClearDevicesChanged() +{ + mIsDevicesChanged = false; +} + +bool +AudioManager::VolumeStreamState::IsVolumeIndexesChanged() +{ + return mIsVolumeIndexesChanged; +} + +void +AudioManager::VolumeStreamState::ClearVolumeIndexesChanged() +{ + mIsVolumeIndexesChanged = false; +} + +void +AudioManager::VolumeStreamState::InitStreamVolume() +{ + AudioSystem::initStreamVolume(static_cast<audio_stream_type_t>(mStreamType), + 0, + GetMaxIndex()); +} + +uint32_t +AudioManager::VolumeStreamState::GetMaxIndex() +{ + return sMaxStreamVolumeTbl[mStreamType]; +} + +uint32_t +AudioManager::VolumeStreamState::GetDefaultIndex() +{ + return sDefaultStreamVolumeTbl[mStreamType]; +} + +uint32_t +AudioManager::VolumeStreamState::GetVolumeIndex() +{ + uint32_t device = mManager.GetDeviceForStream(mStreamType); + return GetVolumeIndex(device); +} + +uint32_t +AudioManager::VolumeStreamState::GetVolumeIndex(uint32_t aDevice) +{ + uint32_t index = 0; + bool ret = mVolumeIndexes.Get(aDevice, &index); + if (!ret) { + index = mVolumeIndexes.Get(AUDIO_DEVICE_OUT_DEFAULT); + } + return index; +} + +nsresult +AudioManager::VolumeStreamState::SetVolumeIndexToActiveDevices(uint32_t aIndex) +{ + uint32_t device = mManager.GetDeviceForStream(mStreamType); + + // Update volume index for device + uint32_t oldVolumeIndex = 0; + bool exist = mVolumeIndexes.Get(device, &oldVolumeIndex); + if (exist && aIndex == oldVolumeIndex) { + // No update + return NS_OK; + } + + // AudioPolicyManager::setStreamVolumeIndex() set volumes of all active + // devices for stream. + nsresult rv; + rv = SetVolumeIndexToConsistentDeviceIfNeeded(aIndex, device); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +AudioManager::VolumeStreamState::SetVolumeIndexToAliasStreams(uint32_t aIndex, + uint32_t aDevice) +{ + uint32_t oldVolumeIndex = 0; + bool exist = mVolumeIndexes.Get(aDevice, &oldVolumeIndex); + if (exist && aIndex == oldVolumeIndex) { + // No update + return NS_OK; + } + + nsresult rv = SetVolumeIndexToConsistentDeviceIfNeeded(aIndex, aDevice); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + for (int32_t streamType = 0; streamType < AUDIO_STREAM_MAX; streamType++) { + if ((streamType != mStreamType) && + sStreamVolumeAliasTbl[streamType] == mStreamType) { + // Rescaling of index is not necessary. + rv = mManager.mStreamStates[streamType]-> + SetVolumeIndexToAliasStreams(aIndex, aDevice); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + return NS_OK; +} + +nsresult +AudioManager::VolumeStreamState::SetVolumeIndexToConsistentDeviceIfNeeded(uint32_t aIndex, uint32_t aDevice) +{ + nsresult rv; + if (aDevice == AUDIO_DEVICE_OUT_SPEAKER || aDevice == AUDIO_DEVICE_OUT_EARPIECE) { + // Set AUDIO_DEVICE_OUT_SPEAKER and AUDIO_DEVICE_OUT_EARPIECE to same volume. + rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_SPEAKER); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = SetVolumeIndex(aIndex, AUDIO_DEVICE_OUT_EARPIECE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // No alias device + rv = SetVolumeIndex(aIndex, aDevice); + } + return rv; +} + +nsresult +AudioManager::VolumeStreamState::SetVolumeIndex(uint32_t aIndex, + uint32_t aDevice, + bool aUpdateCache) +{ + status_t rv; +#if ANDROID_VERSION >= 17 + if (aUpdateCache) { + mVolumeIndexes.Put(aDevice, aIndex); + mIsVolumeIndexesChanged = true; + mManager.AudioOutDeviceUpdated(aDevice); + } + + rv = AudioSystem::setStreamVolumeIndex( + static_cast<audio_stream_type_t>(mStreamType), + aIndex, + aDevice); + return rv ? NS_ERROR_FAILURE : NS_OK; +#else + if (aUpdateCache) { + // Per audio out device volume is not supported. + // Use AUDIO_DEVICE_OUT_SPEAKER just to store audio volume to DB. + mVolumeIndexes.Put(AUDIO_DEVICE_OUT_SPEAKER, aIndex); + mIsVolumeIndexesChanged = true; + mManager.AudioOutDeviceUpdated(AUDIO_DEVICE_OUT_SPEAKER); + } + rv = AudioSystem::setStreamVolumeIndex( + static_cast<audio_stream_type_t>(mStreamType), + aIndex); + return rv ? NS_ERROR_FAILURE : NS_OK; +#endif +} + +void +AudioManager::VolumeStreamState::RestoreVolumeIndexToAllDevices() +{ + for (auto iter = mVolumeIndexes.Iter(); !iter.Done(); iter.Next()) { + const uint32_t& key = iter.Key(); + uint32_t& index = iter.Data(); + SetVolumeIndex(key, index, /* aUpdateCache */ false); + } +} + +} /* namespace gonk */ +} /* namespace dom */ +} /* namespace mozilla */ diff --git a/dom/system/gonk/AudioManager.h b/dom/system/gonk/AudioManager.h new file mode 100644 index 000000000..f56eaad6c --- /dev/null +++ b/dom/system/gonk/AudioManager.h @@ -0,0 +1,180 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +#ifndef mozilla_dom_system_b2g_audiomanager_h__ +#define mozilla_dom_system_b2g_audiomanager_h__ + +#include "mozilla/HalTypes.h" +#include "mozilla/Observer.h" +#include "mozilla/UniquePtr.h" +#include "nsAutoPtr.h" +#include "nsDataHashtable.h" +#include "nsIAudioManager.h" +#include "nsIObserver.h" +#include "android_audio/AudioSystem.h" + +// {b2b51423-502d-4d77-89b3-7786b562b084} +#define NS_AUDIOMANAGER_CID {0x94f6fd70, 0x7615, 0x4af9, \ + {0x89, 0x10, 0xf9, 0x3c, 0x55, 0xe6, 0x62, 0xec}} +#define NS_AUDIOMANAGER_CONTRACTID "@mozilla.org/telephony/audiomanager;1" + +class nsISettingsServiceLock; + +namespace mozilla { +namespace hal { +class SwitchEvent; +typedef Observer<SwitchEvent> SwitchObserver; +} // namespace hal + +namespace dom { +namespace gonk { + +class VolumeInitCallback; + +class AudioManager final : public nsIAudioManager + , public nsIObserver +{ +public: + static already_AddRefed<AudioManager> GetInstance(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIAUDIOMANAGER + NS_DECL_NSIOBSERVER + + // Validate whether the volume index is within the range + nsresult ValidateVolumeIndex(int32_t aStream, uint32_t aIndex) const; + + // Called when android AudioFlinger in mediaserver is died + void HandleAudioFlingerDied(); + + void HandleHeadphoneSwitchEvent(const hal::SwitchEvent& aEvent); + + class VolumeStreamState { + public: + explicit VolumeStreamState(AudioManager& aManager, int32_t aStreamType); + int32_t GetStreamType() + { + return mStreamType; + } + bool IsDevicesChanged(bool aFromCache = true); + void ClearDevicesChanged(); + uint32_t GetLastDevices() + { + return mLastDevices; + } + bool IsVolumeIndexesChanged(); + void ClearVolumeIndexesChanged(); + void InitStreamVolume(); + uint32_t GetMaxIndex(); + uint32_t GetDefaultIndex(); + uint32_t GetVolumeIndex(); + uint32_t GetVolumeIndex(uint32_t aDevice); + void ClearCurrentVolumeUpdated(); + // Set volume index to all active devices. + // Active devices are chosen by android AudioPolicyManager. + nsresult SetVolumeIndexToActiveDevices(uint32_t aIndex); + // Set volume index to all alias streams for device. Alias streams have same volume. + nsresult SetVolumeIndexToAliasStreams(uint32_t aIndex, uint32_t aDevice); + nsresult SetVolumeIndexToConsistentDeviceIfNeeded(uint32_t aIndex, uint32_t aDevice); + nsresult SetVolumeIndex(uint32_t aIndex, uint32_t aDevice, bool aUpdateCache = true); + // Restore volume index to all devices. Called when AudioFlinger is restarted. + void RestoreVolumeIndexToAllDevices(); + private: + AudioManager& mManager; + const int32_t mStreamType; + uint32_t mLastDevices; + bool mIsDevicesChanged; + bool mIsVolumeIndexesChanged; + nsDataHashtable<nsUint32HashKey, uint32_t> mVolumeIndexes; + }; + +protected: + int32_t mPhoneState; + + bool mIsVolumeInited; + + // A bitwise variable for volume update of audio output devices, + // clear it after store the value into database. + uint32_t mAudioOutDevicesUpdated; + + // Connected devices that are controlled by setDeviceConnectionState() + nsDataHashtable<nsUint32HashKey, nsCString> mConnectedDevices; + + nsDataHashtable<nsUint32HashKey, uint32_t> mAudioDeviceTableIdMaps; + + bool mSwitchDone; + +#if defined(MOZ_B2G_BT) || ANDROID_VERSION >= 17 + bool mBluetoothA2dpEnabled; +#endif +#ifdef MOZ_B2G_BT + bool mA2dpSwitchDone; +#endif + nsTArray<UniquePtr<VolumeStreamState> > mStreamStates; + uint32_t mLastChannelVolume[AUDIO_STREAM_CNT]; + + bool IsFmOutConnected(); + + nsresult SetStreamVolumeForDevice(int32_t aStream, + uint32_t aIndex, + uint32_t aDevice); + nsresult SetStreamVolumeIndex(int32_t aStream, uint32_t aIndex); + nsresult GetStreamVolumeIndex(int32_t aStream, uint32_t* aIndex); + + void UpdateCachedActiveDevicesForStreams(); + uint32_t GetDevicesForStream(int32_t aStream, bool aFromCache = true); + uint32_t GetDeviceForStream(int32_t aStream); + // Choose one device as representative of active devices. + static uint32_t SelectDeviceFromDevices(uint32_t aOutDevices); + +private: + nsAutoPtr<mozilla::hal::SwitchObserver> mObserver; + + void HandleBluetoothStatusChanged(nsISupports* aSubject, + const char* aTopic, + const nsCString aAddress); + void HandleAudioChannelProcessChanged(); + + // Append the audio output device to the volume setting string. + nsAutoCString AppendDeviceToVolumeSetting(const char* aName, + uint32_t aDevice); + + // We store the volume setting in the database, these are related functions. + void InitVolumeFromDatabase(); + void MaybeUpdateVolumeSettingToDatabase(bool aForce = false); + + // Promise functions. + void InitDeviceVolumeSucceeded(); + void InitDeviceVolumeFailed(const char* aError); + + void AudioOutDeviceUpdated(uint32_t aDevice); + + void UpdateHeadsetConnectionState(hal::SwitchState aState); + void UpdateDeviceConnectionState(bool aIsConnected, uint32_t aDevice, const nsCString& aDeviceName); + void SetAllDeviceConnectionStates(); + + AudioManager(); + ~AudioManager(); + + friend class VolumeInitCallback; + friend class VolumeStreamState; + friend class GonkAudioPortCallback; +}; + +} /* namespace gonk */ +} /* namespace dom */ +} /* namespace mozilla */ + +#endif // mozilla_dom_system_b2g_audiomanager_h__ diff --git a/dom/system/gonk/AutoMounter.cpp b/dom/system/gonk/AutoMounter.cpp new file mode 100644 index 000000000..52c4554fb --- /dev/null +++ b/dom/system/gonk/AutoMounter.cpp @@ -0,0 +1,1496 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <errno.h> +#include <fcntl.h> +#include <pthread.h> +#include <signal.h> +#include <string.h> +#include <strings.h> +#include <unistd.h> +#include <sys/statfs.h> + +#include <arpa/inet.h> +#include <linux/types.h> +#include <linux/netlink.h> +#include <netinet/in.h> +#include <sys/socket.h> +#include <android/log.h> +#include <cutils/properties.h> + +#include "AutoMounter.h" +#include "nsVolumeService.h" +#include "AutoMounterSetting.h" +#include "base/message_loop.h" +#include "base/task.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Hal.h" +#include "mozilla/StaticPtr.h" +#include "MozMtpServer.h" +#include "MozMtpStorage.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsMemory.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "OpenFileFinder.h" +#include "Volume.h" +#include "VolumeManager.h" +#include "nsIStatusReporter.h" + +USING_MTP_NAMESPACE + +/************************************************************************** +* +* The following "switch" files are available for monitoring usb +* connections: +* +* /sys/devices/virtual/switch/usb_connected/state +* /sys/devices/virtual/switch/usb_configuration/state +* +* Under gingerbread, only the usb_configuration seems to be available. +* Starting with honeycomb, usb_connected was also added. +* +* When a cable insertion/removal occurs, then a uevent similar to the +* following will be generted: +* +* change@/devices/virtual/switch/usb_configuration +* ACTION=change +* DEVPATH=/devices/virtual/switch/usb_configuration +* SUBSYSTEM=switch +* SWITCH_NAME=usb_configuration +* SWITCH_STATE=0 +* SEQNUM=5038 +* +* SWITCH_STATE will be 0 after a removal and 1 after an insertion +* +**************************************************************************/ + +#define USB_CONFIGURATION_SWITCH_NAME NS_LITERAL_STRING("usb_configuration") + +#define GB_SYS_UMS_ENABLE "/sys/devices/virtual/usb_composite/usb_mass_storage/enable" +#define GB_SYS_USB_CONFIGURED "/sys/devices/virtual/switch/usb_configuration/state" + +#define ICS_SYS_USB_FUNCTIONS "/sys/devices/virtual/android_usb/android0/functions" +#define ICS_SYS_UMS_DIRECTORY "/sys/devices/virtual/android_usb/android0/f_mass_storage" +#define ICS_SYS_MTP_DIRECTORY "/sys/devices/virtual/android_usb/android0/f_mtp" +#define ICS_SYS_USB_STATE "/sys/devices/virtual/android_usb/android0/state" + +#undef USE_DEBUG // MozMtpDatabase.h also defines USE_DEBUG +#define USE_DEBUG 0 + +#undef LOG +#undef LOGW +#undef ERR +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "AutoMounter", ## args) +#define LOGW(args...) __android_log_print(ANDROID_LOG_WARN, "AutoMounter", ## args) +#define ERR(args...) __android_log_print(ANDROID_LOG_ERROR, "AutoMounter", ## args) + +#undef DBG +#if USE_DEBUG +#define DBG(args...) __android_log_print(ANDROID_LOG_DEBUG, "AutoMounter" , ## args) +#else +#define DBG(args...) +#endif + +namespace mozilla { +namespace system { + +#define SYS_USB_CONFIG "sys.usb.config" +#define PERSIST_SYS_USB_CONFIG "persist.sys.usb.config" + +#define USB_FUNC_ADB "adb" +#define USB_FUNC_MTP "mtp" +#define USB_FUNC_NONE "none" +#define USB_FUNC_RNDIS "rndis" +#define USB_FUNC_UMS "mass_storage" +#define USB_FUNC_DEFAULT "default" + +class AutoMounter; + +static void SetAutoMounterStatus(int32_t aStatus); + +/***************************************************************************/ + +inline const char* SwitchStateStr(const hal::SwitchEvent& aEvent) +{ + return aEvent.status() == hal::SWITCH_STATE_ON ? "plugged" : "unplugged"; +} + +/***************************************************************************/ + +static bool +IsUsbCablePluggedIn() +{ +#if 0 + // Use this code when bug 745078 gets fixed (or use whatever the + // appropriate method is) + return GetCurrentSwitchEvent(SWITCH_USB) == hal::SWITCH_STATE_ON; +#else + // Until then, just go read the file directly + if (access(ICS_SYS_USB_STATE, F_OK) == 0) { + char usbState[20]; + if (ReadSysFile(ICS_SYS_USB_STATE, usbState, sizeof(usbState))) { + DBG("IsUsbCablePluggedIn: state = '%s'", usbState); + return strcmp(usbState, "CONFIGURED") == 0 || + strcmp(usbState, "CONNECTED") == 0; + } + ERR("Error reading file '%s': %s", ICS_SYS_USB_STATE, strerror(errno)); + return false; + } + bool configured; + if (ReadSysFile(GB_SYS_USB_CONFIGURED, &configured)) { + return configured; + } + ERR("Error reading file '%s': %s", GB_SYS_USB_CONFIGURED, strerror(errno)); + return false; +#endif +} + +static bool +IsUsbConfigured() +{ + if (access(ICS_SYS_USB_STATE, F_OK) == 0) { + char usbState[20]; + if (ReadSysFile(ICS_SYS_USB_STATE, usbState, sizeof(usbState))) { + DBG("IsUsbConfigured: state = '%s'", usbState); + return strcmp(usbState, "CONFIGURED") == 0; + } + ERR("Error reading file '%s': %s", ICS_SYS_USB_STATE, strerror(errno)); + return false; + } + bool configured; + if (ReadSysFile(GB_SYS_USB_CONFIGURED, &configured)) { + return configured; + } + ERR("Error reading file '%s': %s", GB_SYS_USB_CONFIGURED, strerror(errno)); + return false; +} + +/***************************************************************************/ + +// The AutoVolumeManagerStateObserver allows the AutoMounter to know when +// the volume manager changes state (i.e. it has finished initialization) +class AutoVolumeManagerStateObserver : public VolumeManager::StateObserver +{ +public: + virtual void Notify(const VolumeManager::StateChangedEvent& aEvent); +}; + +// The AutoVolumeEventObserver allows the AutoMounter to know about card +// insertion and removal, as well as state changes in the volume. +class AutoVolumeEventObserver : public Volume::EventObserver +{ +public: + virtual void Notify(Volume* const& aEvent); +}; + +class AutoMounterResponseCallback : public VolumeResponseCallback +{ +public: + AutoMounterResponseCallback() + : mErrorCount(0) + { + } + +protected: + virtual void ResponseReceived(const VolumeCommand* aCommand); + +private: + const static int kMaxErrorCount = 3; // Max number of errors before we give up + + int mErrorCount; +}; + +/***************************************************************************/ + +class AutoMounter +{ +public: + NS_INLINE_DECL_REFCOUNTING(AutoMounter) + + typedef nsTArray<RefPtr<Volume>> VolumeArray; + + AutoMounter() + : mState(STATE_IDLE), + mResponseCallback(new AutoMounterResponseCallback), + mMode(AUTOMOUNTER_DISABLE) + { + VolumeManager::RegisterStateObserver(&mVolumeManagerStateObserver); + Volume::RegisterVolumeObserver(&mVolumeEventObserver, "AutoMounter"); + + // It's possible that the VolumeManager is already in the READY state, + // so we call CheckVolumeSettings here to cover that case. Otherwise, + // we'll pick it up when the VolumeManage state changes to VOLUMES_READY. + CheckVolumeSettings(); + + DBG("Calling UpdateState from constructor"); + UpdateState(); + } + + void CheckVolumeSettings() + { + if (VolumeManager::State() != VolumeManager::VOLUMES_READY) { + DBG("CheckVolumeSettings: VolumeManager is NOT READY yet"); + return; + } + DBG("CheckVolumeSettings: VolumeManager is READY"); + + // The VolumeManager knows about all of the volumes from vold. We now + // know the names of all of the volumes, so we can find out what the + // initial sharing settings are set to. + + VolumeManager::VolumeArray::size_type numVolumes = VolumeManager::NumVolumes(); + VolumeManager::VolumeArray::index_type i; + for (i = 0; i < numVolumes; i++) { + RefPtr<Volume> vol = VolumeManager::GetVolume(i); + if (vol) { + // We need to pick up the intial value of the + // ums.volume.NAME.enabled setting. + AutoMounterSetting::CheckVolumeSettings(vol->Name()); + + // Note: eventually CheckVolumeSettings will call + // AutoMounter::SetSharingMode, which will in turn call + // UpdateState if needed. + } + } + } + + void UpdateState(); + void GetStatus(bool& umsAvail, bool& umsConfigured, bool& umsEnabled, bool& mtpAvail, + bool& mtpConfigured, bool& mtpEnabled, bool& rndisConfigured); + + nsresult Dump(nsACString& desc); + + void ConfigureUsbFunction(const char* aUsbFunc); + + bool StartMtpServer(); + void StopMtpServer(); + + void StartUmsSharing(); + void StopUmsSharing(); + + + const char* ModeStr(int32_t aMode) + { + switch (aMode) { + case AUTOMOUNTER_DISABLE: return "Disable"; + case AUTOMOUNTER_ENABLE_UMS: return "Enable-UMS"; + case AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED: return "DisableWhenUnplugged"; + case AUTOMOUNTER_ENABLE_MTP: return "Enable-MTP"; + } + return "??? Unknown ???"; + } + + bool IsModeEnabled(int32_t aMode) + { + return aMode == AUTOMOUNTER_ENABLE_MTP || + aMode == AUTOMOUNTER_ENABLE_UMS; + } + + void SetMode(int32_t aMode) + { + if ((aMode == AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED) && + (mMode == AUTOMOUNTER_DISABLE)) { + // If it's already disabled, then leave it as disabled. + // AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED implies "enabled until unplugged" + aMode = AUTOMOUNTER_DISABLE; + } + + if (aMode == AUTOMOUNTER_DISABLE && + mMode == AUTOMOUNTER_ENABLE_UMS && IsUsbCablePluggedIn()) { + // On many devices (esp non-Samsung), we can't force the disable, so we + // need to defer until the USB cable is actually unplugged. + // See bug 777043. + // + // Otherwise our attempt to disable it will fail, and we'll wind up in a bad + // state where the AutoMounter thinks that Sharing has been turned off, but + // the files are actually still being Shared because the attempt to unshare + // failed. + LOG("Attempting to disable UMS. Deferring until USB cable is unplugged."); + aMode = AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED; + } + + if (aMode != mMode) { + LOG("Changing mode from '%s' to '%s'", ModeStr(mMode), ModeStr(aMode)); + mMode = aMode; + DBG("Calling UpdateState due to mode set to %d", mMode); + UpdateState(); + } + } + + void SetSharingMode(const nsACString& aVolumeName, bool aAllowSharing) + { + RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName); + if (!vol) { + return; + } + if (vol->IsSharingEnabled() == aAllowSharing) { + return; + } + vol->SetUnmountRequested(false); + vol->SetMountRequested(false); + vol->SetSharingEnabled(aAllowSharing); + DBG("Calling UpdateState due to volume %s sharing set to %d", + vol->NameStr(), (int)aAllowSharing); + UpdateState(); + } + + void FormatVolume(const nsACString& aVolumeName) + { + RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName); + if (!vol) { + return; + } + if (vol->IsFormatRequested()) { + return; + } + vol->SetUnmountRequested(false); + vol->SetMountRequested(false); + vol->SetFormatRequested(true); + DBG("Calling UpdateState due to volume %s formatting set to %d", + vol->NameStr(), (int)vol->IsFormatRequested()); + UpdateState(); + } + + void MountVolume(const nsACString& aVolumeName) + { + RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName); + if (!vol) { + return; + } + vol->SetUnmountRequested(false); + if (vol->IsMountRequested() || vol->mState == nsIVolume::STATE_MOUNTED) { + return; + } + vol->SetMountRequested(true); + DBG("Calling UpdateState due to volume %s mounting set to %d", + vol->NameStr(), (int)vol->IsMountRequested()); + UpdateState(); + } + + void UnmountVolume(const nsACString& aVolumeName) + { + RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName); + if (!vol) { + return; + } + if (vol->IsUnmountRequested()) { + return; + } + vol->SetMountRequested(false); + vol->SetUnmountRequested(true); + DBG("Calling UpdateState due to volume %s unmounting set to %d", + vol->NameStr(), (int)vol->IsUnmountRequested()); + UpdateState(); + } + +protected: + ~AutoMounter() + { + Volume::UnregisterVolumeObserver(&mVolumeEventObserver, "AutoMounter"); + VolumeManager::UnregisterStateObserver(&mVolumeManagerStateObserver); + } + +private: + + enum STATE + { + // IDLE - Nothing is being shared + STATE_IDLE, + + // We've detected that conditions are right to enable mtp. So we've + // set sys.usb.config to include mtp, and we're waiting for the USB + // subsystem to be "configured". Once mtp shows up in + // then we know + // that its been configured and we can open /dev/mtp_usb + STATE_MTP_CONFIGURING, + + // mtp has been configured (i.e. mtp now shows up in + // /sys/devices/virtual/android_usb/android0/functions so we can start + // the mtp server. + STATE_MTP_STARTED, + + // The mtp server has reported sessionStarted. We'll leave this state + // when we receive sessionEnded. + STATE_MTP_CONNECTED, + + // We've added mass_storage (aka UMS) to sys.usb.config and we're waiting for + // mass_storage to appear in /sys/devices/virtual/android_usb/android0/functions + STATE_UMS_CONFIGURING, + + // mass_storage has been configured and we can start sharing once the user + // enables it. + STATE_UMS_CONFIGURED, + + // USB Tethering is enabled + STATE_RNDIS_CONFIGURED, + }; + + const char *StateStr(STATE aState) + { + switch (aState) { + case STATE_IDLE: return "IDLE"; + case STATE_MTP_CONFIGURING: return "MTP_CONFIGURING"; + case STATE_MTP_CONNECTED: return "MTP_CONNECTED"; + case STATE_MTP_STARTED: return "MTP_STARTED"; + case STATE_UMS_CONFIGURING: return "UMS_CONFIGURING"; + case STATE_UMS_CONFIGURED: return "UMS_CONFIGURED"; + case STATE_RNDIS_CONFIGURED: return "RNDIS_CONFIGURED"; + } + return "STATE_???"; + } + + void SetState(STATE aState) + { + const char *oldStateStr = StateStr(mState); + mState = aState; + const char *newStateStr = StateStr(mState); + LOG("AutoMounter state changed from %s to %s", oldStateStr, newStateStr); + } + + STATE mState; + + AutoVolumeEventObserver mVolumeEventObserver; + AutoVolumeManagerStateObserver mVolumeManagerStateObserver; + RefPtr<VolumeResponseCallback> mResponseCallback; + int32_t mMode; + MozMtpStorage::Array mMozMtpStorage; +}; + +static StaticRefPtr<AutoMounter> sAutoMounter; +static StaticRefPtr<MozMtpServer> sMozMtpServer; + +// The following is for status reporter +enum STATE_REPORTER_STATE +{ + REPORTER_UNREGISTERED, + REPORTER_REGISTERED +}; + +static int status_reporter_progress = REPORTER_UNREGISTERED; +nsresult getState(nsACString &desc) +{ + sAutoMounter->Dump(desc); + return NS_OK; +} +NS_STATUS_REPORTER_IMPLEMENT(AutoMounter, "AutoMounter", getState); + +/***************************************************************************/ + +void +AutoVolumeManagerStateObserver::Notify(const VolumeManager::StateChangedEvent &) +{ + LOG("VolumeManager state changed event: %s", VolumeManager::StateStr()); + + if (!sAutoMounter) { + return; + } + + // In the event that the VolumeManager just entered the VOLUMES_READY state, + // we call CheckVolumeSettings here (it's possible that this method never + // gets called if the VolumeManager was already in the VOLUMES_READY state + // by the time the AutoMounter was constructed). + sAutoMounter->CheckVolumeSettings(); + + DBG("Calling UpdateState due to VolumeManagerStateObserver"); + sAutoMounter->UpdateState(); +} + +void +AutoVolumeEventObserver::Notify(Volume * const &) +{ + if (!sAutoMounter) { + return; + } + DBG("Calling UpdateState due to VolumeEventStateObserver"); + sAutoMounter->UpdateState(); +} + +void +AutoMounterResponseCallback::ResponseReceived(const VolumeCommand* aCommand) +{ + + if (WasSuccessful()) { + DBG("Calling UpdateState due to Volume::OnSuccess"); + mErrorCount = 0; + sAutoMounter->UpdateState(); + return; + } + ERR("Command '%s' failed: %d '%s'", + aCommand->CmdStr(), ResponseCode(), ResponseStr().get()); + + if (++mErrorCount < kMaxErrorCount) { + DBG("Calling UpdateState due to VolumeResponseCallback::OnError"); + sAutoMounter->UpdateState(); + } +} + +static bool +IsUsbFunctionEnabled(const char* aConfig, const char* aUsbFunc) +{ + nsAutoCString config(aConfig); + nsCCharSeparatedTokenizer tokenizer(config, ','); + + while (tokenizer.hasMoreTokens()) { + nsAutoCString token(tokenizer.nextToken()); + if (token.Equals(aUsbFunc)) { + DBG("IsUsbFunctionEnabled('%s', '%s'): returning true", aConfig, aUsbFunc); + return true; + } + } + DBG("IsUsbFunctionEnabled('%s', '%s'): returning false", aConfig, aUsbFunc); + return false; +} + +static void +SetUsbFunction(const char* aUsbFunc) +{ + char oldSysUsbConfig[PROPERTY_VALUE_MAX]; + property_get(SYS_USB_CONFIG, oldSysUsbConfig, ""); + + if (strcmp(oldSysUsbConfig, USB_FUNC_NONE) == 0) { + // It's quite possible that sys.usb.config may have the value "none". We + // convert that to an empty string here, and at the end we convert the + // empty string back to "none". + oldSysUsbConfig[0] = '\0'; + } + + if (IsUsbFunctionEnabled(oldSysUsbConfig, aUsbFunc)) { + // The function is already configured. Nothing else to do. + DBG("SetUsbFunction('%s') - already set - nothing to do", aUsbFunc); + return; + } + + char newSysUsbConfig[PROPERTY_VALUE_MAX]; + + if (strcmp(aUsbFunc, USB_FUNC_MTP) == 0) { + // We're enabling MTP. For this we'll wind up using mtp, or mtp,adb + strlcpy(newSysUsbConfig, USB_FUNC_MTP, sizeof(newSysUsbConfig)); + } else if (strcmp(aUsbFunc, USB_FUNC_UMS) == 0) { + // We're enabling UMS. For this we make the assumption that the persisted + // property has mass_storage enabled. + property_get(PERSIST_SYS_USB_CONFIG, newSysUsbConfig, ""); + } else if (strcmp(aUsbFunc, USB_FUNC_DEFAULT) == 0) { + // Set the property as PERSIST_SYS_USB_CONFIG + property_get(PERSIST_SYS_USB_CONFIG, newSysUsbConfig, ""); + } else { + printf_stderr("AutoMounter::SetUsbFunction Unrecognized aUsbFunc '%s'\n", aUsbFunc); + MOZ_ASSERT(0); + return; + } + + // Make sure the new value that we write into sys.usb.config keeps the adb + // (or non-adb) of the current string. + + if (IsUsbFunctionEnabled(oldSysUsbConfig, USB_FUNC_ADB)) { + // ADB was turned on - keep it on. + if (!IsUsbFunctionEnabled(newSysUsbConfig, USB_FUNC_ADB)) { + // Add adb to the new string + strlcat(newSysUsbConfig, ",", sizeof(newSysUsbConfig)); + strlcat(newSysUsbConfig, USB_FUNC_ADB, sizeof(newSysUsbConfig)); + } + } else { + // ADB was turned off - keep it off + if (IsUsbFunctionEnabled(newSysUsbConfig, USB_FUNC_ADB)) { + // Remove ADB from the new string. + if (strcmp(newSysUsbConfig, USB_FUNC_ADB) == 0) { + newSysUsbConfig[0] = '\0'; + } else { + nsAutoCString withoutAdb(newSysUsbConfig); + withoutAdb.ReplaceSubstring( "," USB_FUNC_ADB, ""); + strlcpy(newSysUsbConfig, withoutAdb.get(), sizeof(newSysUsbConfig)); + } + } + } + + // If the persisted function didn't have mass_storage (this happens on + // the nexus 4/5, then we can get to here and have oldSysUsbConfig equal + // to newSysUsbConfig. So we need to check for that. + + if (strcmp(oldSysUsbConfig, newSysUsbConfig) == 0) { + DBG("SetUsbFunction('%s') %s is already set to '%s' - nothing to do", + aUsbFunc, SYS_USB_CONFIG, newSysUsbConfig); + return; + } + + if (newSysUsbConfig[0] == '\0') { + // Convert the empty string back to the string "none" + strlcpy(newSysUsbConfig, USB_FUNC_NONE, sizeof(newSysUsbConfig)); + } + LOG("SetUsbFunction(%s) %s from '%s' to '%s'", aUsbFunc, SYS_USB_CONFIG, + oldSysUsbConfig, newSysUsbConfig); + property_set(SYS_USB_CONFIG, newSysUsbConfig); +} + +bool +AutoMounter::StartMtpServer() +{ + if (sMozMtpServer) { + // Mtp Server is already running - nothing to do + return true; + } + LOG("Starting MtpServer"); + + // For debugging, Change the #if 0 to #if 1, and then attach gdb during + // the 5 second interval below. Otherwise, configuring MTP will cause adb + // (and thus gdb) to get bounced. +#if 0 + LOG("Sleeping"); + PRTime now = PR_Now(); + PRTime stopTime = now + 5000000; + while (PR_Now() < stopTime) { + LOG("Sleeping..."); + sleep(1); + } + LOG("Sleep done"); +#endif + + sMozMtpServer = new MozMtpServer(); + if (!sMozMtpServer->Init()) { + sMozMtpServer = nullptr; + return false; + } + + VolumeArray::index_type volIndex; + VolumeArray::size_type numVolumes = VolumeManager::NumVolumes(); + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex); + RefPtr<MozMtpStorage> storage = new MozMtpStorage(vol, sMozMtpServer); + mMozMtpStorage.AppendElement(storage); + } + + sMozMtpServer->Run(); + return true; +} + +void +AutoMounter::StopMtpServer() +{ + LOG("Stopping MtpServer"); + + mMozMtpStorage.Clear(); + sMozMtpServer = nullptr; +} + +/***************************************************************************/ + +void +AutoMounter::UpdateState() +{ + static bool inUpdateState = false; + if (inUpdateState) { + // When UpdateState calls SetISharing, this causes a volume state + // change to occur, which would normally cause UpdateState to be called + // again. We want the volume state change to go out (so that device + // storage will see the change in sharing state), but since we're + // already in UpdateState we just want to prevent recursion from screwing + // things up. + return; + } + AutoRestore<bool> inUpdateStateDetector(inUpdateState); + inUpdateState = true; + + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + // If the following preconditions are met: + // - UMS is available (i.e. compiled into the kernel) + // - UMS is enabled + // - AutoMounter is enabled + // - USB cable is plugged in + // then we will try to unmount and share + // otherwise we will try to unshare and mount. + + if (VolumeManager::State() != VolumeManager::VOLUMES_READY) { + // The volume manager isn't in a ready state, so there + // isn't anything else that we can do. + LOG("UpdateState: VolumeManager not ready yet"); + return; + } + + if (mResponseCallback->IsPending()) { + // We only deal with one outstanding volume command at a time, + // so we need to wait for it to finish. + return; + } + + // Calling setprop sys.usb.config mtp,adb (or adding mass_storage) will + // cause /sys/devices/virtual/android_usb/android0/state to go: + // CONFIGURED -> DISCONNECTED -> CONNECTED -> CONFIGURED + // + // Since IsUsbCablePluggedIn returns state == CONFIGURED, it will look + // like a cable pull and replugin. + bool umsAvail, umsConfigured, umsEnabled; + bool mtpAvail, mtpConfigured, mtpEnabled; + bool rndisConfigured; + bool usbCablePluggedIn = IsUsbCablePluggedIn(); + GetStatus(umsAvail, umsConfigured, umsEnabled, mtpAvail, + mtpConfigured, mtpEnabled, rndisConfigured); + bool enabled = mtpEnabled || umsEnabled; + + if (mMode == AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED) { + // DISABLE_WHEN_UNPLUGGED implies already enabled. + enabled = usbCablePluggedIn; + if (!usbCablePluggedIn) { + mMode = AUTOMOUNTER_DISABLE; + mtpEnabled = false; + umsEnabled = false; + } + } + + DBG("UpdateState: ums:A%dC%dE%d mtp:A%dC%dE%d rndis:%d mode:%d usb:%d mState:%s", + umsAvail, umsConfigured, umsEnabled, + mtpAvail, mtpConfigured, mtpEnabled, rndisConfigured, + mMode, usbCablePluggedIn, StateStr(mState)); + + switch (mState) { + + case STATE_IDLE: + if (!usbCablePluggedIn) { + // Stay in the IDLE state. We'll get a CONNECTED or CONFIGURED + // UEvent when the usb cable is plugged in. + break; + } + if (rndisConfigured) { + // USB Tethering uses RNDIS. We'll just wait until its turned off. + SetState(STATE_RNDIS_CONFIGURED); + break; + } + if (mtpEnabled) { + if (mtpConfigured) { + // The USB layer has already been configured. Now we can go ahead + // and start the MTP server. This particular codepath will not + // normally be taken, but it could happen if you stop and restart + // b2g while sys.usb.config is set to enable mtp. + if (StartMtpServer()) { + SetState(STATE_MTP_STARTED); + } else { + if (umsAvail) { + // Unable to start MTP. Go back to UMS. + LOG("UpdateState: StartMtpServer failed, switch to UMS"); + SetUsbFunction(USB_FUNC_UMS); + SetState(STATE_UMS_CONFIGURING); + } else { + LOG("UpdateState: StartMtpServer failed, keep idle state"); + SetUsbFunction(USB_FUNC_DEFAULT); + } + } + } else { + // We need to configure USB to use mtp. Wait for it to be configured + // before we start the MTP server. + SetUsbFunction(USB_FUNC_MTP); + SetState(STATE_MTP_CONFIGURING); + } + } else if (umsConfigured) { + // UMS is already configured. + SetState(STATE_UMS_CONFIGURED); + } else if (umsAvail) { + // We do this whether or not UMS is enabled. With UMS, it's the + // sharing of the volume which is significant. What is important + // is that we don't leave it in MTP mode when MTP isn't enabled. + SetUsbFunction(USB_FUNC_UMS); + SetState(STATE_UMS_CONFIGURING); + } + break; + + case STATE_MTP_CONFIGURING: + // While configuring, the USB configuration state will change from + // CONFIGURED -> CONNECTED -> DISCONNECTED -> CONNECTED -> CONFIGURED + // so we don't check for cable unplugged here. + if (mtpEnabled && mtpConfigured) { + // The USB layer has been configured. Now we can go ahead and start + // the MTP server. + if (StartMtpServer()) { + SetState(STATE_MTP_STARTED); + } else { + // Unable to start MTP. Go back to UMS. + SetUsbFunction(USB_FUNC_UMS); + SetState(STATE_UMS_CONFIGURING); + } + break; + } + if (rndisConfigured) { + SetState(STATE_RNDIS_CONFIGURED); + break; + } + break; + + case STATE_MTP_STARTED: + if (usbCablePluggedIn && mtpConfigured && mtpEnabled) { + // Everything is still good. Leave the MTP server running + break; + } + DBG("STATE_MTP_STARTED: About to StopMtpServer " + "mtpConfigured = %d mtpEnabled = %d usbCablePluggedIn: %d", + mtpConfigured, mtpEnabled, usbCablePluggedIn); + StopMtpServer(); + if (rndisConfigured) { + SetState(STATE_RNDIS_CONFIGURED); + break; + } + if (umsAvail) { + // Switch back to UMS + SetUsbFunction(USB_FUNC_UMS); + SetState(STATE_UMS_CONFIGURING); + break; + } + + // if ums/rndis is not available and mtp is disable, + // restore the usb function as PERSIST_SYS_USB_CONFIG. + SetUsbFunction(USB_FUNC_DEFAULT); + SetState(STATE_IDLE); + break; + + case STATE_UMS_CONFIGURING: + if (mtpEnabled) { + // MTP was enabled. Start reconfiguring. + SetState(STATE_MTP_CONFIGURING); + SetUsbFunction(USB_FUNC_MTP); + break; + } + if (rndisConfigured) { + SetState(STATE_RNDIS_CONFIGURED); + break; + } + // While configuring, the USB configuration state will change from + // CONFIGURED -> CONNECTED -> DISCONNECTED -> CONNECTED -> CONFIGURED + // so we don't check for cable unplugged here. However, having said + // that, we'll often sit in this state while the cable is unplugged, + // since we might not get any events until the cable gets plugged back + // in. This is why we need to check for mtpEnabled once we get the + // configured event. + if (umsConfigured) { + SetState(STATE_UMS_CONFIGURED); + } + break; + + case STATE_UMS_CONFIGURED: + if (usbCablePluggedIn) { + if (mtpEnabled) { + // MTP was enabled. Start reconfiguring. + SetState(STATE_MTP_CONFIGURING); + SetUsbFunction(USB_FUNC_MTP); + break; + } + if (umsConfigured && umsEnabled) { + // This is the normal state when UMS is enabled. + break; + } + } + if (rndisConfigured) { + SetState(STATE_RNDIS_CONFIGURED); + break; + } + SetState(STATE_IDLE); + break; + + case STATE_RNDIS_CONFIGURED: + if (usbCablePluggedIn && rndisConfigured) { + // Normal state when RNDIS is enabled. + break; + } + SetState(STATE_IDLE); + break; + + default: + SetState(STATE_IDLE); + break; + } + + bool tryToShare = umsEnabled && usbCablePluggedIn; + LOG("UpdateState: ums:A%dC%dE%d mtp:A%dC%dE%d mode:%d usb:%d tryToShare:%d state:%s", + umsAvail, umsConfigured, umsEnabled, + mtpAvail, mtpConfigured, mtpEnabled, + mMode, usbCablePluggedIn, tryToShare, StateStr(mState)); + + bool filesOpen = false; + static unsigned filesOpenDelayCount = 0; + VolumeArray::index_type volIndex; + VolumeArray::size_type numVolumes = VolumeManager::NumVolumes(); + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex); + Volume::STATE volState = vol->State(); + + if (vol->State() == nsIVolume::STATE_MOUNTED) { + LOG("UpdateState: Volume %s is %s and %s @ %s gen %d locked %d sharing %s", + vol->NameStr(), vol->StateStr(), + vol->MediaPresent() ? "inserted" : "missing", + vol->MountPoint().get(), vol->MountGeneration(), + (int)vol->IsMountLocked(), + vol->CanBeShared() ? (vol->IsSharingEnabled() ? + (vol->IsSharing() ? "en-y" : "en-n") : "dis") : "x"); + if (vol->IsSharing() && !usbCablePluggedIn) { + // We call SetIsSharing(true) below to indicate intent to share. This + // causes a state change which notifys apps, and they'll close any + // open files, which will initiate the change away from the mounted + // state and into the sharing state. Normally, when the volume + // transitions back to the mounted state, then vol->mIsSharing gets set + // to false. However, if the user pulls the USB cable before we + // actually start sharing, then the volume never actually leaves + // the mounted state (and hence never transitions from + // sharing -> mounted), and mIsSharing never gets set back to false. + // So we clear mIsSharing here. + vol->SetIsSharing(false); + } + } else { + LOG("UpdateState: Volume %s is %s and %s", vol->NameStr(), vol->StateStr(), + vol->MediaPresent() ? "inserted" : "missing"); + } + if (!vol->MediaPresent()) { + // No media - nothing we can do + continue; + } + + if (vol->State() == nsIVolume::STATE_CHECKMNT) { + // vold reports the volume is "Mounted". Need to check if the volume is + // accessible by statfs(). Once it can be accessed, set the volume as + // STATE_MOUNTED, otherwise, post a delay task of UpdateState to check it + // again. + struct statfs fsbuf; + int rc = MOZ_TEMP_FAILURE_RETRY(statfs(vol->MountPoint().get(), &fsbuf)); + if (rc == -1) { + // statfs() failed. Stay in STATE_CHECKMNT. Any failures here + // are probably non-recoverable, so we need to wait until + // something else changes the state back to IDLE/UNMOUNTED, etc. + ERR("statfs failed for '%s': errno = %d (%s)", vol->NameStr(), errno, strerror(errno)); + continue; + } + static int delay = 250; + if (fsbuf.f_blocks == 0) { + if (delay <= 4000) { + LOG("UpdateState: Volume '%s' is inaccessible, checking again in %d msec", vol->NameStr(), delay); + MessageLoopForIO::current()-> + PostDelayedTask(NewRunnableMethod(this, &AutoMounter::UpdateState), + delay); + delay *= 2; + } else { + LOG("UpdateState: Volume '%s' is inaccessible, giving up", vol->NameStr()); + } + continue; + } else { + delay = 250; + vol->SetState(nsIVolume::STATE_MOUNTED); + } + } + + if ((tryToShare && vol->IsSharingEnabled()) || + vol->IsFormatRequested() || + vol->IsUnmountRequested()) { + switch (volState) { + // We're going to try to unmount the volume + case nsIVolume::STATE_MOUNTED: { + if (vol->IsMountLocked()) { + // The volume is currently locked, so leave it in the mounted + // state. + LOGW("UpdateState: Mounted volume %s is locked, not sharing or formatting", + vol->NameStr()); + break; + } + + // Mark the volume as if we've started sharing/formatting/unmmounting. + // This will cause apps which watch device storage notifications to see + // the volume go into the different state, and prompt them to close any + // open files that they might have. + if (tryToShare && vol->IsSharingEnabled()) { + vol->SetIsSharing(true); + } else if (vol->IsFormatRequested()){ + vol->SetIsFormatting(true); + } else if (vol->IsUnmountRequested()){ + vol->SetIsUnmounting(true); + } + + // Check to see if there are any open files on the volume and + // don't initiate the unmount while there are open files. + OpenFileFinder::Info fileInfo; + OpenFileFinder fileFinder(vol->MountPoint()); + if (fileFinder.First(&fileInfo)) { + LOGW("The following files are open under '%s'", + vol->MountPoint().get()); + do { + LOGW(" PID: %d file: '%s' app: '%s' comm: '%s' exe: '%s'\n", + fileInfo.mPid, + fileInfo.mFileName.get(), + fileInfo.mAppName.get(), + fileInfo.mComm.get(), + fileInfo.mExe.get()); + } while (fileFinder.Next(&fileInfo)); + LOGW("UpdateState: Mounted volume %s has open files, not sharing or formatting", + vol->NameStr()); + + // Check again in a few seconds to see if the files are closed. + // Since we're trying to share the volume, this implies that we're + // plugged into the PC via USB and this in turn implies that the + // battery is charging, so we don't need to be too concerned about + // wasting battery here. + // + // If we just detected that there were files open, then we use + // a short timer. We will have told the apps that we're trying + // trying to share, and they'll be closing their files. This makes + // the sharing more responsive. If after a few seconds, the apps + // haven't closed their files, then we back off. + + int delay = 1000; + if (filesOpenDelayCount > 10) { + delay = 5000; + } + MessageLoopForIO::current()-> + PostDelayedTask(NewRunnableMethod(this, &AutoMounter::UpdateState), + delay); + filesOpen = true; + break; + } + + // Volume is mounted, we need to unmount before + // we can share. + LOG("UpdateState: Unmounting %s", vol->NameStr()); + vol->StartUnmount(mResponseCallback); + return; // UpdateState will be called again when the Unmount command completes + } + case nsIVolume::STATE_IDLE: + case nsIVolume::STATE_MOUNT_FAIL: { + LOG("UpdateState: Volume %s is %s", vol->NameStr(), vol->StateStr()); + if (vol->IsFormatting() && !vol->IsFormatRequested()) { + vol->SetFormatRequested(false); + if (!(tryToShare && vol->IsSharingEnabled()) && volState == nsIVolume::STATE_IDLE) { + LOG("UpdateState: Mounting %s", vol->NameStr()); + vol->StartMount(mResponseCallback); + break; + } + } + + // If there are format and share requests in the same time, + // we should do format first then share. + if (vol->IsFormatRequested()) { + // Volume is unmounted. We can go ahead and format. + LOG("UpdateState: Formatting %s", vol->NameStr()); + vol->StartFormat(mResponseCallback); + } else if (tryToShare && vol->IsSharingEnabled() && volState == nsIVolume::STATE_IDLE) { + // Volume is unmounted. We can go ahead and share. + LOG("UpdateState: Sharing %s", vol->NameStr()); + vol->StartShare(mResponseCallback); + } + return; // UpdateState will be called again when the Share/Format command completes + } + default: { + // Not in a state that we can do anything about. + break; + } + } + } else { + // We're going to try and unshare and remount the volumes + switch (volState) { + case nsIVolume::STATE_SHARED: { + // Volume is shared. We can go ahead and unshare. + LOG("UpdateState: Unsharing %s", vol->NameStr()); + vol->StartUnshare(mResponseCallback); + return; // UpdateState will be called again when the Unshare command completes + } + case nsIVolume::STATE_IDLE: { + if (!vol->IsUnmountRequested()) { + // Volume is unmounted and mount-requested, try to mount. + + LOG("UpdateState: Mounting %s", vol->NameStr()); + vol->StartMount(mResponseCallback); + } + return; // UpdateState will be called again when Mount command completes + } + default: { + // Not in a state that we can do anything about. + break; + } + } + } + } + + int32_t status = AUTOMOUNTER_STATUS_DISABLED; + if (filesOpen) { + filesOpenDelayCount++; + status = AUTOMOUNTER_STATUS_FILES_OPEN; + } else if (enabled) { + filesOpenDelayCount = 0; + status = AUTOMOUNTER_STATUS_ENABLED; + } + SetAutoMounterStatus(status); +} + +/***************************************************************************/ + +void AutoMounter::GetStatus(bool& umsAvail, bool& umsConfigured, bool& umsEnabled, + bool& mtpAvail, bool& mtpConfigured, bool& mtpEnabled, + bool& rndisConfigured) +{ + umsAvail = false; + umsConfigured = false; + umsEnabled = false; + mtpAvail = false; + mtpConfigured = false; + mtpEnabled = false; + rndisConfigured = false; + + if (access(ICS_SYS_USB_FUNCTIONS, F_OK) != 0) { + return; + } + + char functionsStr[60]; + if (!ReadSysFile(ICS_SYS_USB_FUNCTIONS, functionsStr, sizeof(functionsStr))) { + ERR("Error reading file '%s': %s", ICS_SYS_USB_FUNCTIONS, strerror(errno)); + functionsStr[0] = '\0'; + } + DBG("GetStatus: USB functions: '%s'", functionsStr); + + bool usbConfigured = IsUsbConfigured(); + + // On the Nexus 4/5, it advertises that the UMS usb function is available, + // but we have a further requirement that mass_storage be in the + // persist.sys.usb.config property. + char persistSysUsbConfig[PROPERTY_VALUE_MAX]; + property_get(PERSIST_SYS_USB_CONFIG, persistSysUsbConfig, ""); + if (IsUsbFunctionEnabled(persistSysUsbConfig, USB_FUNC_UMS)) { + umsAvail = (access(ICS_SYS_UMS_DIRECTORY, F_OK) == 0); + } + if (umsAvail) { + umsConfigured = usbConfigured && strstr(functionsStr, USB_FUNC_UMS) != nullptr; + umsEnabled = (mMode == AUTOMOUNTER_ENABLE_UMS) || + ((mMode == AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED) && umsConfigured); + } else { + umsConfigured = false; + umsEnabled = false; + } + + mtpAvail = (access(ICS_SYS_MTP_DIRECTORY, F_OK) == 0); + if (mtpAvail) { + mtpConfigured = usbConfigured && strstr(functionsStr, USB_FUNC_MTP) != nullptr; + mtpEnabled = (mMode == AUTOMOUNTER_ENABLE_MTP) || + ((mMode == AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED) && mtpConfigured); + } else { + mtpConfigured = false; + mtpEnabled = false; + } + + rndisConfigured = strstr(functionsStr, USB_FUNC_RNDIS) != nullptr; +} + + +nsresult AutoMounter::Dump(nsACString& desc) +{ + DBG("GetState!"); + bool umsAvail, umsConfigured, umsEnabled; + bool mtpAvail, mtpConfigured, mtpEnabled; + bool rndisConfigured; + GetStatus(umsAvail, umsConfigured, umsEnabled, mtpAvail, + mtpConfigured, mtpEnabled, rndisConfigured); + if (mMode == AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED) { + // DISABLE_WHEN_UNPLUGGED implies already enabled. + if (!IsUsbCablePluggedIn()) { + mMode = AUTOMOUNTER_DISABLE; + mtpEnabled = false; + umsEnabled = false; + } + } + + // Automounter information + desc += "Current Mode:"; + desc += ModeStr(mMode); + desc += "|"; + + + desc += "Current State:"; + desc += StateStr(mState); + desc += "|"; + + desc += "UMS Status:"; + if (umsAvail) { + desc += "Available"; + } else { + desc += "UnAvailable"; + } + desc += ","; + if (umsConfigured) { + desc += "Configured"; + } else { + desc += "Un-Configured"; + } + desc += ","; + if (umsEnabled) { + desc += "Enabled"; + } else { + desc += "Disabled"; + } + desc += "|"; + + + desc += "MTP Status:"; + if (mtpAvail) { + desc += "Available"; + } else { + desc += "UnAvailable"; + } + desc += ","; + if (mtpConfigured) { + desc += "Configured"; + } else { + desc += "Un-Configured"; + } + desc += ","; + if (mtpEnabled) { + desc += "Enabled"; + } else { + desc += "Disabled"; + } + + + // Volume information + VolumeArray::index_type volIndex; + VolumeArray::size_type numVolumes = VolumeManager::NumVolumes(); + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex); + + desc += "|"; + desc += vol->NameStr(); + desc += ":"; + desc += vol->StateStr(); + desc += "@"; + desc += vol->MountPoint().get(); + + if (!vol->MediaPresent()) { + continue; + } + + if (vol->CanBeShared()) { + desc += ",CanBeShared"; + } + if (vol->CanBeFormatted()) { + desc += ",CanBeFormatted"; + } + if (vol->CanBeMounted()) { + desc += ",CanBeMounted"; + } + if (vol->IsRemovable()) { + desc += ",Removable"; + } + if (vol->IsHotSwappable()) { + desc += ",HotSwappable"; + } + } + + return NS_OK; +} + + +static void +InitAutoMounterIOThread() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + MOZ_ASSERT(!sAutoMounter); + + sAutoMounter = new AutoMounter(); +} + +static void +ShutdownAutoMounterIOThread() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + sAutoMounter = nullptr; + ShutdownVolumeManager(); +} + +static void +SetAutoMounterModeIOThread(const int32_t& aMode) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + MOZ_ASSERT(sAutoMounter); + + sAutoMounter->SetMode(aMode); +} + +static void +SetAutoMounterSharingModeIOThread(const nsCString& aVolumeName, const bool& aAllowSharing) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + MOZ_ASSERT(sAutoMounter); + + sAutoMounter->SetSharingMode(aVolumeName, aAllowSharing); +} + +static void +AutoMounterFormatVolumeIOThread(const nsCString& aVolumeName) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + MOZ_ASSERT(sAutoMounter); + + sAutoMounter->FormatVolume(aVolumeName); +} + +static void +AutoMounterMountVolumeIOThread(const nsCString& aVolumeName) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + MOZ_ASSERT(sAutoMounter); + + sAutoMounter->MountVolume(aVolumeName); +} + +static void +AutoMounterUnmountVolumeIOThread(const nsCString& aVolumeName) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + MOZ_ASSERT(sAutoMounter); + + sAutoMounter->UnmountVolume(aVolumeName); +} + +static void +UsbCableEventIOThread() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + if (!sAutoMounter) { + return; + } + DBG("Calling UpdateState due to USBCableEvent"); + sAutoMounter->UpdateState(); +} + +/************************************************************************** +* +* Public API +* +* Since the AutoMounter runs in IO Thread context, we need to switch +* to IOThread context before we can do anything. +* +**************************************************************************/ + +class UsbCableObserver final : public hal::SwitchObserver +{ + ~UsbCableObserver() + { + hal::UnregisterSwitchObserver(hal::SWITCH_USB, this); + } + +public: + NS_INLINE_DECL_REFCOUNTING(UsbCableObserver) + + UsbCableObserver() + { + hal::RegisterSwitchObserver(hal::SWITCH_USB, this); + } + + virtual void Notify(const hal::SwitchEvent& aEvent) + { + DBG("UsbCable switch device: %d state: %s\n", + aEvent.device(), SwitchStateStr(aEvent)); + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(UsbCableEventIOThread)); + } +}; + +static StaticRefPtr<UsbCableObserver> sUsbCableObserver; +static StaticRefPtr<AutoMounterSetting> sAutoMounterSetting; + +void +InitAutoMounter() +{ + InitVolumeManager(); + sAutoMounterSetting = new AutoMounterSetting(); + + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(InitAutoMounterIOThread)); + + // Switch Observers need to run on the main thread, so we need to + // start it here and have it send events to the AutoMounter running + // on the IO Thread. + sUsbCableObserver = new UsbCableObserver(); + + // Register status reporter into reporter manager + if(status_reporter_progress == REPORTER_UNREGISTERED) { + NS_RegisterStatusReporter(new NS_STATUS_REPORTER_NAME(AutoMounter)); + } + status_reporter_progress = REPORTER_REGISTERED; +} + +int32_t +GetAutoMounterStatus() +{ + if (sAutoMounterSetting) { + return sAutoMounterSetting->GetStatus(); + } + return AUTOMOUNTER_STATUS_DISABLED; +} + +//static +void +SetAutoMounterStatus(int32_t aStatus) +{ + if (sAutoMounterSetting) { + sAutoMounterSetting->SetStatus(aStatus); + } +} + +void +SetAutoMounterMode(int32_t aMode) +{ + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(SetAutoMounterModeIOThread, aMode)); +} + +void +SetAutoMounterSharingMode(const nsCString& aVolumeName, bool aAllowSharing) +{ + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(SetAutoMounterSharingModeIOThread, + aVolumeName, aAllowSharing)); +} + +void +AutoMounterFormatVolume(const nsCString& aVolumeName) +{ + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(AutoMounterFormatVolumeIOThread, + aVolumeName)); +} + +void +AutoMounterMountVolume(const nsCString& aVolumeName) +{ + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(AutoMounterMountVolumeIOThread, + aVolumeName)); +} + +void +AutoMounterUnmountVolume(const nsCString& aVolumeName) +{ + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(AutoMounterUnmountVolumeIOThread, + aVolumeName)); +} + +void +ShutdownAutoMounter() +{ + if (sAutoMounter) { + DBG("ShutdownAutoMounter: About to StopMtpServer"); + sAutoMounter->StopMtpServer(); + // Unregister status reporter into reporter manager + if(status_reporter_progress == REPORTER_REGISTERED) { + NS_UnregisterStatusReporter(new NS_STATUS_REPORTER_NAME(AutoMounter)); + } + status_reporter_progress = REPORTER_UNREGISTERED; + } + sAutoMounterSetting = nullptr; + sUsbCableObserver = nullptr; + + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(ShutdownAutoMounterIOThread)); +} + +} // system +} // mozilla diff --git a/dom/system/gonk/AutoMounter.h b/dom/system/gonk/AutoMounter.h new file mode 100644 index 000000000..ea98cadf1 --- /dev/null +++ b/dom/system/gonk/AutoMounter.h @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_automounter_h__ +#define mozilla_system_automounter_h__ + +#include <stdint.h> + +class nsCString; + +namespace mozilla { +namespace system { + +// AutoMounter modes +#define AUTOMOUNTER_DISABLE 0 +#define AUTOMOUNTER_ENABLE_UMS 1 +#define AUTOMOUNTER_DISABLE_WHEN_UNPLUGGED 2 +#define AUTOMOUNTER_ENABLE_MTP 3 + +// Automounter statuses +#define AUTOMOUNTER_STATUS_DISABLED 0 +#define AUTOMOUNTER_STATUS_ENABLED 1 +#define AUTOMOUNTER_STATUS_FILES_OPEN 2 + +/** + * Initialize the automounter. This causes some of the phone's + * directories to show up on the host when the phone is plugged + * into the host via USB. + * + * When the AutoMounter starts, it will poll the current state + * of affairs (usb cable plugged in, automounter enabled, etc) + * and try to make the state of the volumes match. + */ +void +InitAutoMounter(); + +/** + * Sets the enabled state of the automounter. + * + * This will in turn cause the automounter to re-evaluate + * whether it should mount/unmount/share/unshare volumes. + */ +void +SetAutoMounterMode(int32_t aMode); + +/** + * Reports the status of the automounter. + */ +int32_t +GetAutoMounterStatus(); + +/** + * Sets the sharing mode of an individual volume. + * + * If a volume is enabled for sharing, and the autmounter + * is in a state to share, then the volume will be shared + * with the PC. + */ +void +SetAutoMounterSharingMode(const nsCString& aVolumeName, bool aAllowSharing); + +/** + * Formats the volume with specified volume name. + * + * If the volume is ready to format, automounter + * will unmount it, format it and then mount it again. + */ +void +AutoMounterFormatVolume(const nsCString& aVolumeName); + +/** + * Mounts the volume with specified volume name. + * + * If the volume is already unmounted, automounter + * will mount it. Otherwise automounter will skip this. + */ +void +AutoMounterMountVolume(const nsCString& aVolumeName); + +/** + * Unmounts the volume with specified volume name. + * + * If the volume is already mounted, automounter + * will unmount it. Otherwise automounter will skip this. + */ +void +AutoMounterUnmountVolume(const nsCString& aVolumeName); + +/** + * Shuts down the automounter. + * + * This leaves the volumes in whatever state they're in. + */ +void +ShutdownAutoMounter(); + +} // system +} // mozilla + +#endif // mozilla_system_automounter_h__ diff --git a/dom/system/gonk/AutoMounterSetting.cpp b/dom/system/gonk/AutoMounterSetting.cpp new file mode 100644 index 000000000..606bcce04 --- /dev/null +++ b/dom/system/gonk/AutoMounterSetting.cpp @@ -0,0 +1,284 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AutoMounter.h" +#include "AutoMounterSetting.h" + +#include "base/message_loop.h" +#include "jsapi.h" +#include "mozilla/Services.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsIObserverService.h" +#include "nsISettingsService.h" +#include "nsJSUtils.h" +#include "nsPrintfCString.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "xpcpublic.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" + +#undef LOG +#undef ERR +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "AutoMounterSetting" , ## args) +#define ERR(args...) __android_log_print(ANDROID_LOG_ERROR, "AutoMounterSetting" , ## args) + +#define UMS_MODE "ums.mode" +#define UMS_STATUS "ums.status" +#define UMS_VOLUME_ENABLED_PREFIX "ums.volume." +#define UMS_VOLUME_ENABLED_SUFFIX ".enabled" +#define MOZSETTINGS_CHANGED "mozsettings-changed" + +using namespace mozilla::dom; + +namespace mozilla { +namespace system { + +class SettingsServiceCallback final : public nsISettingsServiceCallback +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + SettingsServiceCallback() {} + + NS_IMETHOD Handle(const nsAString& aName, JS::Handle<JS::Value> aResult) + { + if (aResult.isInt32()) { + int32_t mode = aResult.toInt32(); + SetAutoMounterMode(mode); + } + return NS_OK; + } + + NS_IMETHOD HandleError(const nsAString& aName) + { + ERR("SettingsCallback::HandleError: %s\n", NS_LossyConvertUTF16toASCII(aName).get()); + return NS_OK; + } + +protected: + ~SettingsServiceCallback() {} +}; + +NS_IMPL_ISUPPORTS(SettingsServiceCallback, nsISettingsServiceCallback) + +class CheckVolumeSettingsCallback final : public nsISettingsServiceCallback +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + CheckVolumeSettingsCallback(const nsACString& aVolumeName) + : mVolumeName(aVolumeName) {} + + NS_IMETHOD Handle(const nsAString& aName, JS::Handle<JS::Value> aResult) + { + if (aResult.isBoolean()) { + bool isSharingEnabled = aResult.toBoolean(); + SetAutoMounterSharingMode(mVolumeName, isSharingEnabled); + } + return NS_OK; + } + + NS_IMETHOD HandleError(const nsAString& aName) + { + ERR("CheckVolumeSettingsCallback::HandleError: %s\n", NS_LossyConvertUTF16toASCII(aName).get()); + return NS_OK; + } + +protected: + ~CheckVolumeSettingsCallback() {} + +private: + nsCString mVolumeName; +}; + +NS_IMPL_ISUPPORTS(CheckVolumeSettingsCallback, nsISettingsServiceCallback) + +AutoMounterSetting::AutoMounterSetting() + : mStatus(AUTOMOUNTER_STATUS_DISABLED) +{ + MOZ_ASSERT(NS_IsMainThread()); + + // Setup an observer to watch changes to the setting + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) { + ERR("GetObserverService failed"); + return; + } + nsresult rv; + rv = observerService->AddObserver(this, MOZSETTINGS_CHANGED, false); + if (NS_FAILED(rv)) { + ERR("AddObserver failed"); + return; + } + + // Force ums.mode to be 0 initially. We do this because settings are persisted. + // We don't want UMS to be enabled until such time as the phone is unlocked, + // and gaia/apps/system/js/storage.js takes care of detecting when the phone + // becomes unlocked and changes ums.mode appropriately. + nsCOMPtr<nsISettingsService> settingsService = + do_GetService("@mozilla.org/settingsService;1"); + if (!settingsService) { + ERR("Failed to get settingsLock service!"); + return; + } + nsCOMPtr<nsISettingsServiceLock> lock; + settingsService->CreateLock(nullptr, getter_AddRefs(lock)); + nsCOMPtr<nsISettingsServiceCallback> callback = new SettingsServiceCallback(); + JS::Rooted<JS::Value> value(RootingCx()); + value.setInt32(AUTOMOUNTER_DISABLE); + lock->Set(UMS_MODE, value, callback, nullptr); + value.setInt32(mStatus); + lock->Set(UMS_STATUS, value, nullptr, nullptr); +} + +AutoMounterSetting::~AutoMounterSetting() +{ + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, MOZSETTINGS_CHANGED); + } +} + +NS_IMPL_ISUPPORTS(AutoMounterSetting, nsIObserver) + +const char * +AutoMounterSetting::StatusStr(int32_t aStatus) +{ + switch (aStatus) { + case AUTOMOUNTER_STATUS_DISABLED: return "Disabled"; + case AUTOMOUNTER_STATUS_ENABLED: return "Enabled"; + case AUTOMOUNTER_STATUS_FILES_OPEN: return "FilesOpen"; + } + return "??? Unknown ???"; +} + +class CheckVolumeSettingsRunnable : public Runnable +{ +public: + CheckVolumeSettingsRunnable(const nsACString& aVolumeName) + : mVolumeName(aVolumeName) {} + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsISettingsService> settingsService = + do_GetService("@mozilla.org/settingsService;1"); + NS_ENSURE_TRUE(settingsService, NS_ERROR_FAILURE); + nsCOMPtr<nsISettingsServiceLock> lock; + settingsService->CreateLock(nullptr, getter_AddRefs(lock)); + nsCOMPtr<nsISettingsServiceCallback> callback = + new CheckVolumeSettingsCallback(mVolumeName); + nsPrintfCString setting(UMS_VOLUME_ENABLED_PREFIX "%s" UMS_VOLUME_ENABLED_SUFFIX, + mVolumeName.get()); + lock->Get(setting.get(), callback); + return NS_OK; + } + +private: + nsCString mVolumeName; +}; + +//static +void +AutoMounterSetting::CheckVolumeSettings(const nsACString& aVolumeName) +{ + NS_DispatchToMainThread(new CheckVolumeSettingsRunnable(aVolumeName)); +} + +class SetStatusRunnable : public Runnable +{ +public: + SetStatusRunnable(int32_t aStatus) : mStatus(aStatus) {} + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr<nsISettingsService> settingsService = + do_GetService("@mozilla.org/settingsService;1"); + NS_ENSURE_TRUE(settingsService, NS_ERROR_FAILURE); + nsCOMPtr<nsISettingsServiceLock> lock; + settingsService->CreateLock(nullptr, getter_AddRefs(lock)); + // lock may be null if this gets called during shutdown. + if (lock) { + JS::Rooted<JS::Value> value(RootingCx(), + JS::Int32Value(mStatus)); + lock->Set(UMS_STATUS, value, nullptr, nullptr); + } + return NS_OK; + } + +private: + int32_t mStatus; +}; + +//static +void +AutoMounterSetting::SetStatus(int32_t aStatus) +{ + if (aStatus != mStatus) { + LOG("Changing status from '%s' to '%s'", + StatusStr(mStatus), StatusStr(aStatus)); + mStatus = aStatus; + NS_DispatchToMainThread(new SetStatusRunnable(aStatus)); + } +} + +NS_IMETHODIMP +AutoMounterSetting::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + if (strcmp(aTopic, MOZSETTINGS_CHANGED) != 0) { + return NS_OK; + } + + // Note that this function gets called for any and all settings changes, + // so we need to carefully check if we have the one we're interested in. + // + // The string that we're interested in will be a JSON string that looks like: + // {"key":"ums.autoMount","value":true} + + RootedDictionary<SettingChangeNotification> setting(RootingCx()); + if (!WrappedJSToDictionary(aSubject, setting)) { + return NS_OK; + } + + // Check for ums.mode changes + if (setting.mKey.EqualsASCII(UMS_MODE)) { + if (!setting.mValue.isInt32()) { + return NS_OK; + } + int32_t mode = setting.mValue.toInt32(); + SetAutoMounterMode(mode); + return NS_OK; + } + + // Check for ums.volume.NAME.enabled + if (StringBeginsWith(setting.mKey, NS_LITERAL_STRING(UMS_VOLUME_ENABLED_PREFIX)) && + StringEndsWith(setting.mKey, NS_LITERAL_STRING(UMS_VOLUME_ENABLED_SUFFIX))) { + if (!setting.mValue.isBoolean()) { + return NS_OK; + } + const size_t prefixLen = sizeof(UMS_VOLUME_ENABLED_PREFIX) - 1; + const size_t suffixLen = sizeof(UMS_VOLUME_ENABLED_SUFFIX) - 1; + nsDependentSubstring volumeName = + Substring(setting.mKey, prefixLen, setting.mKey.Length() - prefixLen - suffixLen); + bool isSharingEnabled = setting.mValue.toBoolean(); + SetAutoMounterSharingMode(NS_LossyConvertUTF16toASCII(volumeName), isSharingEnabled); + return NS_OK; + } + + return NS_OK; +} + +} // namespace system +} // namespace mozilla diff --git a/dom/system/gonk/AutoMounterSetting.h b/dom/system/gonk/AutoMounterSetting.h new file mode 100644 index 000000000..7972de379 --- /dev/null +++ b/dom/system/gonk/AutoMounterSetting.h @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_automountersetting_h__ +#define mozilla_system_automountersetting_h__ + +#include "nsIObserver.h" + +namespace mozilla { +namespace system { + +class AutoMounterSetting : public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + AutoMounterSetting(); + + static void CheckVolumeSettings(const nsACString& aVolumeName); + + int32_t GetStatus() { return mStatus; } + void SetStatus(int32_t aStatus); + const char *StatusStr(int32_t aStatus); + +protected: + virtual ~AutoMounterSetting(); + +private: + int32_t mStatus; +}; + +} // namespace system +} // namespace mozilla + +#endif // mozilla_system_automountersetting_h__ + diff --git a/dom/system/gonk/DataCallInterfaceService.js b/dom/system/gonk/DataCallInterfaceService.js new file mode 100644 index 000000000..0f0e7101c --- /dev/null +++ b/dom/system/gonk/DataCallInterfaceService.js @@ -0,0 +1,276 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const DATACALLINTERFACE_CONTRACTID = "@mozilla.org/datacall/interface;1"; +const DATACALLINTERFACESERVICE_CONTRACTID = + "@mozilla.org/datacall/interfaceservice;1"; +const DATACALLINTERFACE_CID = + Components.ID("{ff669306-4390-462a-989b-ba37fc42153f}"); +const DATACALLINTERFACESERVICE_CID = + Components.ID("{e23e9337-592d-40b9-8cef-7bd47c28b72e}"); + +const TOPIC_XPCOM_SHUTDOWN = "xpcom-shutdown"; +const TOPIC_PREF_CHANGED = "nsPref:changed"; +const PREF_RIL_DEBUG_ENABLED = "ril.debugging.enabled"; + +XPCOMUtils.defineLazyGetter(this, "RIL", function () { + let obj = {}; + Cu.import("resource://gre/modules/ril_consts.js", obj); + return obj; +}); + +XPCOMUtils.defineLazyServiceGetter(this, "gRil", + "@mozilla.org/ril;1", + "nsIRadioInterfaceLayer"); + +XPCOMUtils.defineLazyServiceGetter(this, "gMobileConnectionService", + "@mozilla.org/mobileconnection/mobileconnectionservice;1", + "nsIMobileConnectionService"); + +var DEBUG = RIL.DEBUG_RIL; + +function updateDebugFlag() { + // Read debug setting from pref + let debugPref; + try { + debugPref = Services.prefs.getBoolPref(PREF_RIL_DEBUG_ENABLED); + } catch (e) { + debugPref = false; + } + DEBUG = debugPref || RIL.DEBUG_RIL; +} +updateDebugFlag(); + +function DataCall(aAttributes) { + for (let key in aAttributes) { + if (key === "pdpType") { + // Convert pdp type into constant int value. + this[key] = RIL.RIL_DATACALL_PDP_TYPES.indexOf(aAttributes[key]); + continue; + } + + this[key] = aAttributes[key]; + } +} +DataCall.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCall]), + + failCause: Ci.nsIDataCallInterface.DATACALL_FAIL_NONE, + suggestedRetryTime: -1, + cid: -1, + active: -1, + pdpType: -1, + ifname: null, + addreses: null, + dnses: null, + gateways: null, + pcscf: null, + mtu: -1 +}; + +function DataCallInterfaceService() { + this._dataCallInterfaces = []; + + let numClients = gRil.numRadioInterfaces; + for (let i = 0; i < numClients; i++) { + this._dataCallInterfaces.push(new DataCallInterface(i)); + } + + Services.obs.addObserver(this, TOPIC_XPCOM_SHUTDOWN, false); + Services.prefs.addObserver(PREF_RIL_DEBUG_ENABLED, this, false); +} +DataCallInterfaceService.prototype = { + classID: DATACALLINTERFACESERVICE_CID, + classInfo: XPCOMUtils.generateCI({ + classID: DATACALLINTERFACESERVICE_CID, + contractID: DATACALLINTERFACESERVICE_CONTRACTID, + classDescription: "Data Call Interface Service", + interfaces: [Ci.nsIDataCallInterfaceService, + Ci.nsIGonkDataCallInterfaceService] + }), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallInterfaceService, + Ci.nsIGonkDataCallInterfaceService], + Ci.nsIObserver), + + // An array of DataCallInterface instances. + _dataCallInterfaces: null, + + debug: function(aMessage) { + dump("-*- DataCallInterfaceService: " + aMessage + "\n"); + }, + + // nsIDataCallInterfaceService + + getDataCallInterface: function(aClientId) { + let dataCallInterface = this._dataCallInterfaces[aClientId]; + if (!dataCallInterface) { + throw Cr.NS_ERROR_UNEXPECTED; + } + + return dataCallInterface; + }, + + // nsIGonkDataCallInterfaceService + + notifyDataCallListChanged: function(aClientId, aCount, aDataCalls) { + let dataCallInterface = this.getDataCallInterface(aClientId); + dataCallInterface.handleDataCallListChanged(aCount, aDataCalls); + }, + + // nsIObserver + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case TOPIC_PREF_CHANGED: + if (aData === PREF_RIL_DEBUG_ENABLED) { + updateDebugFlag(); + } + break; + case TOPIC_XPCOM_SHUTDOWN: + Services.prefs.removeObserver(PREF_RIL_DEBUG_ENABLED, this); + Services.obs.removeObserver(this, TOPIC_XPCOM_SHUTDOWN); + break; + } + }, +}; + +function DataCallInterface(aClientId) { + this._clientId = aClientId; + this._radioInterface = gRil.getRadioInterface(aClientId); + this._listeners = []; + + if (DEBUG) this.debug("DataCallInterface: " + aClientId); +} +DataCallInterface.prototype = { + classID: DATACALLINTERFACE_CID, + classInfo: XPCOMUtils.generateCI({classID: DATACALLINTERFACE_CID, + contractID: DATACALLINTERFACE_CONTRACTID, + classDescription: "Data Call Interface", + interfaces: [Ci.nsIDataCallInterface]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallInterface]), + + debug: function(aMessage) { + dump("-*- DataCallInterface[" + this._clientId + "]: " + aMessage + "\n"); + }, + + _clientId: -1, + + _radioInterface: null, + + _listeners: null, + + // nsIDataCallInterface + + setupDataCall: function(aApn, aUsername, aPassword, aAuthType, aPdpType, + aCallback) { + let connection = + gMobileConnectionService.getItemByServiceId(this._clientId); + let dataInfo = connection && connection.data; + let radioTechType = dataInfo.type; + let radioTechnology = RIL.GECKO_RADIO_TECH.indexOf(radioTechType); + // Convert pdp type into string value. + let pdpType = RIL.RIL_DATACALL_PDP_TYPES[aPdpType]; + + this._radioInterface.sendWorkerMessage("setupDataCall", { + radioTech: radioTechnology, + apn: aApn, + user: aUsername, + passwd: aPassword, + chappap: aAuthType, + pdptype: pdpType + }, (aResponse) => { + if (aResponse.errorMsg) { + aCallback.notifyError(aResponse.errorMsg); + } else { + let dataCall = new DataCall(aResponse); + aCallback.notifySetupDataCallSuccess(dataCall); + } + }); + }, + + deactivateDataCall: function(aCid, aReason, aCallback) { + this._radioInterface.sendWorkerMessage("deactivateDataCall", { + cid: aCid, + reason: aReason + }, (aResponse) => { + if (aResponse.errorMsg) { + aCallback.notifyError(aResponse.errorMsg); + } else { + aCallback.notifySuccess(); + } + }); + }, + + getDataCallList: function(aCallback) { + this._radioInterface.sendWorkerMessage("getDataCallList", null, + (aResponse) => { + if (aResponse.errorMsg) { + aCallback.notifyError(aResponse.errorMsg); + } else { + let dataCalls = aResponse.datacalls.map( + dataCall => new DataCall(dataCall)); + aCallback.notifyGetDataCallListSuccess(dataCalls.length, dataCalls); + } + }); + }, + + setDataRegistration: function(aAttach, aCallback) { + this._radioInterface.sendWorkerMessage("setDataRegistration", { + attach: aAttach + }, (aResponse) => { + if (aResponse.errorMsg) { + aCallback.notifyError(aResponse.errorMsg); + } else { + aCallback.notifySuccess(); + } + }); + }, + + handleDataCallListChanged: function(aCount, aDataCalls) { + this._notifyAllListeners("notifyDataCallListChanged", [aCount, aDataCalls]); + }, + + _notifyAllListeners: function(aMethodName, aArgs) { + let listeners = this._listeners.slice(); + for (let listener of listeners) { + if (this._listeners.indexOf(listener) == -1) { + // Listener has been unregistered in previous run. + continue; + } + + let handler = listener[aMethodName]; + try { + handler.apply(listener, aArgs); + } catch (e) { + if (DEBUG) { + this.debug("listener for " + aMethodName + " threw an exception: " + e); + } + } + } + }, + + registerListener: function(aListener) { + if (this._listeners.indexOf(aListener) >= 0) { + return; + } + + this._listeners.push(aListener); + }, + + unregisterListener: function(aListener) { + let index = this._listeners.indexOf(aListener); + if (index >= 0) { + this._listeners.splice(index, 1); + } + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataCallInterfaceService]);
\ No newline at end of file diff --git a/dom/system/gonk/DataCallInterfaceService.manifest b/dom/system/gonk/DataCallInterfaceService.manifest new file mode 100644 index 000000000..bf062c7e9 --- /dev/null +++ b/dom/system/gonk/DataCallInterfaceService.manifest @@ -0,0 +1,6 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +component {e23e9337-592d-40b9-8cef-7bd47c28b72e} DataCallInterfaceService.js +contract @mozilla.org/datacall/interfaceservice;1 {e23e9337-592d-40b9-8cef-7bd47c28b72e}
\ No newline at end of file diff --git a/dom/system/gonk/DataCallManager.js b/dom/system/gonk/DataCallManager.js new file mode 100644 index 000000000..5411987cd --- /dev/null +++ b/dom/system/gonk/DataCallManager.js @@ -0,0 +1,1726 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/systemlibs.js"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager", + "@mozilla.org/network/manager;1", + "nsINetworkManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gMobileConnectionService", + "@mozilla.org/mobileconnection/mobileconnectionservice;1", + "nsIMobileConnectionService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gIccService", + "@mozilla.org/icc/iccservice;1", + "nsIIccService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gDataCallInterfaceService", + "@mozilla.org/datacall/interfaceservice;1", + "nsIDataCallInterfaceService"); + +XPCOMUtils.defineLazyGetter(this, "RIL", function() { + let obj = {}; + Cu.import("resource://gre/modules/ril_consts.js", obj); + return obj; +}); + +// Ril quirk to attach data registration on demand. +var RILQUIRKS_DATA_REGISTRATION_ON_DEMAND = + libcutils.property_get("ro.moz.ril.data_reg_on_demand", "false") == "true"; + +// Ril quirk to control the uicc/data subscription. +var RILQUIRKS_SUBSCRIPTION_CONTROL = + libcutils.property_get("ro.moz.ril.subscription_control", "false") == "true"; + +// Ril quirk to enable IPv6 protocol/roaming protocol in APN settings. +var RILQUIRKS_HAVE_IPV6 = + libcutils.property_get("ro.moz.ril.ipv6", "false") == "true"; + +const DATACALLMANAGER_CID = + Components.ID("{35b9efa2-e42c-45ce-8210-0a13e6f4aadc}"); +const DATACALLHANDLER_CID = + Components.ID("{132b650f-c4d8-4731-96c5-83785cb31dee}"); +const RILNETWORKINTERFACE_CID = + Components.ID("{9574ee84-5d0d-4814-b9e6-8b279e03dcf4}"); +const RILNETWORKINFO_CID = + Components.ID("{dd6cf2f0-f0e3-449f-a69e-7c34fdcb8d4b}"); + +const TOPIC_XPCOM_SHUTDOWN = "xpcom-shutdown"; +const TOPIC_MOZSETTINGS_CHANGED = "mozsettings-changed"; +const TOPIC_PREF_CHANGED = "nsPref:changed"; +const TOPIC_DATA_CALL_ERROR = "data-call-error"; +const PREF_RIL_DEBUG_ENABLED = "ril.debugging.enabled"; + +const NETWORK_TYPE_UNKNOWN = Ci.nsINetworkInfo.NETWORK_TYPE_UNKNOWN; +const NETWORK_TYPE_WIFI = Ci.nsINetworkInfo.NETWORK_TYPE_WIFI; +const NETWORK_TYPE_MOBILE = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE; +const NETWORK_TYPE_MOBILE_MMS = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS; +const NETWORK_TYPE_MOBILE_SUPL = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_SUPL; +const NETWORK_TYPE_MOBILE_IMS = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_IMS; +const NETWORK_TYPE_MOBILE_DUN = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN; +const NETWORK_TYPE_MOBILE_FOTA = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_FOTA; + +const NETWORK_STATE_UNKNOWN = Ci.nsINetworkInfo.NETWORK_STATE_UNKNOWN; +const NETWORK_STATE_CONNECTING = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTING; +const NETWORK_STATE_CONNECTED = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED; +const NETWORK_STATE_DISCONNECTING = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTING; +const NETWORK_STATE_DISCONNECTED = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED; + +const INT32_MAX = 2147483647; + +// set to true in ril_consts.js to see debug messages +var DEBUG = RIL.DEBUG_RIL; + +function updateDebugFlag() { + // Read debug setting from pref + let debugPref; + try { + debugPref = Services.prefs.getBoolPref(PREF_RIL_DEBUG_ENABLED); + } catch (e) { + debugPref = false; + } + DEBUG = debugPref || RIL.DEBUG_RIL; +} +updateDebugFlag(); + +function DataCallManager() { + this._connectionHandlers = []; + + let numRadioInterfaces = gMobileConnectionService.numItems; + for (let clientId = 0; clientId < numRadioInterfaces; clientId++) { + this._connectionHandlers.push(new DataCallHandler(clientId)); + } + + let lock = gSettingsService.createLock(); + // Read the APN data from the settings DB. + lock.get("ril.data.apnSettings", this); + // Read the data enabled setting from DB. + lock.get("ril.data.enabled", this); + lock.get("ril.data.roaming_enabled", this); + // Read the default client id for data call. + lock.get("ril.data.defaultServiceId", this); + + Services.obs.addObserver(this, TOPIC_XPCOM_SHUTDOWN, false); + Services.obs.addObserver(this, TOPIC_MOZSETTINGS_CHANGED, false); + Services.prefs.addObserver(PREF_RIL_DEBUG_ENABLED, this, false); +} +DataCallManager.prototype = { + classID: DATACALLMANAGER_CID, + classInfo: XPCOMUtils.generateCI({classID: DATACALLMANAGER_CID, + classDescription: "Data Call Manager", + interfaces: [Ci.nsIDataCallManager]}), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallManager, + Ci.nsIObserver, + Ci.nsISettingsServiceCallback]), + + _connectionHandlers: null, + + // Flag to determine the data state to start with when we boot up. It + // corresponds to the 'ril.data.enabled' setting from the UI. + _dataEnabled: false, + + // Flag to record the default client id for data call. It corresponds to + // the 'ril.data.defaultServiceId' setting from the UI. + _dataDefaultClientId: -1, + + // Flag to record the current default client id for data call. + // It differs from _dataDefaultClientId in that it is set only when + // the switch of client id process is done. + _currentDataClientId: -1, + + // Pending function to execute when we are notified that another data call has + // been disconnected. + _pendingDataCallRequest: null, + + debug: function(aMsg) { + dump("-*- DataCallManager: " + aMsg + "\n"); + }, + + get dataDefaultServiceId() { + return this._dataDefaultClientId; + }, + + getDataCallHandler: function(aClientId) { + let handler = this._connectionHandlers[aClientId] + if (!handler) { + throw Cr.NS_ERROR_UNEXPECTED; + } + + return handler; + }, + + _setDataRegistration: function(aDataCallInterface, aAttach) { + return new Promise(function(aResolve, aReject) { + let callback = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallCallback]), + notifySuccess: function() { + aResolve(); + }, + notifyError: function(aErrorMsg) { + aReject(aErrorMsg); + } + }; + + aDataCallInterface.setDataRegistration(aAttach, callback); + }); + }, + + _handleDataClientIdChange: function(aNewClientId) { + if (this._dataDefaultClientId === aNewClientId) { + return; + } + this._dataDefaultClientId = aNewClientId; + + // This is to handle boot up stage. + if (this._currentDataClientId == -1) { + this._currentDataClientId = this._dataDefaultClientId; + let connHandler = this._connectionHandlers[this._currentDataClientId]; + let dcInterface = connHandler.dataCallInterface; + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND || + RILQUIRKS_SUBSCRIPTION_CONTROL) { + this._setDataRegistration(dcInterface, true); + } + if (this._dataEnabled) { + let settings = connHandler.dataCallSettings; + settings.oldEnabled = settings.enabled; + settings.enabled = true; + connHandler.updateRILNetworkInterface(); + } + return; + } + + let oldConnHandler = this._connectionHandlers[this._currentDataClientId]; + let oldIface = oldConnHandler.dataCallInterface; + let oldSettings = oldConnHandler.dataCallSettings; + let newConnHandler = this._connectionHandlers[this._dataDefaultClientId]; + let newIface = newConnHandler.dataCallInterface; + let newSettings = newConnHandler.dataCallSettings; + + let applyPendingDataSettings = () => { + if (DEBUG) { + this.debug("Apply pending data registration and settings."); + } + + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND || + RILQUIRKS_SUBSCRIPTION_CONTROL) { + this._setDataRegistration(oldIface, false).then(() => { + if (this._dataEnabled) { + newSettings.oldEnabled = newSettings.enabled; + newSettings.enabled = true; + } + this._currentDataClientId = this._dataDefaultClientId; + + this._setDataRegistration(newIface, true).then(() => { + newConnHandler.updateRILNetworkInterface(); + }); + }); + return; + } + + if (this._dataEnabled) { + newSettings.oldEnabled = newSettings.enabled; + newSettings.enabled = true; + } + + this._currentDataClientId = this._dataDefaultClientId; + newConnHandler.updateRILNetworkInterface(); + }; + + if (this._dataEnabled) { + oldSettings.oldEnabled = oldSettings.enabled; + oldSettings.enabled = false; + } + + oldConnHandler.deactivateDataCallsAndWait().then(() => { + applyPendingDataSettings(); + }); + }, + + _shutdown: function() { + for (let handler of this._connectionHandlers) { + handler.shutdown(); + } + this._connectionHandlers = null; + Services.prefs.removeObserver(PREF_RIL_DEBUG_ENABLED, this); + Services.obs.removeObserver(this, TOPIC_XPCOM_SHUTDOWN); + Services.obs.removeObserver(this, TOPIC_MOZSETTINGS_CHANGED); + }, + + /** + * nsISettingsServiceCallback + */ + handle: function(aName, aResult) { + switch (aName) { + case "ril.data.apnSettings": + if (DEBUG) { + this.debug("'ril.data.apnSettings' is now " + + JSON.stringify(aResult)); + } + if (!aResult) { + break; + } + for (let clientId in this._connectionHandlers) { + let handler = this._connectionHandlers[clientId]; + let apnSetting = aResult[clientId]; + if (handler && apnSetting) { + handler.updateApnSettings(apnSetting); + } + } + break; + case "ril.data.enabled": + if (DEBUG) { + this.debug("'ril.data.enabled' is now " + aResult); + } + if (this._dataEnabled === aResult) { + break; + } + this._dataEnabled = aResult; + + if (DEBUG) { + this.debug("Default id for data call: " + this._dataDefaultClientId); + } + if (this._dataDefaultClientId === -1) { + // We haven't got the default id for data from db. + break; + } + + let connHandler = this._connectionHandlers[this._dataDefaultClientId]; + let settings = connHandler.dataCallSettings; + settings.oldEnabled = settings.enabled; + settings.enabled = aResult; + connHandler.updateRILNetworkInterface(); + break; + case "ril.data.roaming_enabled": + if (DEBUG) { + this.debug("'ril.data.roaming_enabled' is now " + aResult); + this.debug("Default id for data call: " + this._dataDefaultClientId); + } + for (let clientId = 0; clientId < this._connectionHandlers.length; clientId++) { + let connHandler = this._connectionHandlers[clientId]; + let settings = connHandler.dataCallSettings; + settings.roamingEnabled = Array.isArray(aResult) ? aResult[clientId] + : aResult; + } + if (this._dataDefaultClientId === -1) { + // We haven't got the default id for data from db. + break; + } + this._connectionHandlers[this._dataDefaultClientId].updateRILNetworkInterface(); + break; + case "ril.data.defaultServiceId": + aResult = aResult || 0; + if (DEBUG) { + this.debug("'ril.data.defaultServiceId' is now " + aResult); + } + this._handleDataClientIdChange(aResult); + break; + } + }, + + handleError: function(aErrorMessage) { + if (DEBUG) { + this.debug("There was an error while reading RIL settings."); + } + }, + + /** + * nsIObserver interface methods. + */ + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case TOPIC_MOZSETTINGS_CHANGED: + if ("wrappedJSObject" in aSubject) { + aSubject = aSubject.wrappedJSObject; + } + this.handle(aSubject.key, aSubject.value); + break; + case TOPIC_PREF_CHANGED: + if (aData === PREF_RIL_DEBUG_ENABLED) { + updateDebugFlag(); + } + break; + case TOPIC_XPCOM_SHUTDOWN: + this._shutdown(); + break; + } + }, +}; + +function DataCallHandler(aClientId) { + // Initial owning attributes. + this.clientId = aClientId; + this.dataCallSettings = { + oldEnabled: false, + enabled: false, + roamingEnabled: false + }; + this._dataCalls = []; + this._listeners = []; + + // This map is used to collect all the apn types and its corresponding + // RILNetworkInterface. + this.dataNetworkInterfaces = new Map(); + + this.dataCallInterface = gDataCallInterfaceService.getDataCallInterface(aClientId); + this.dataCallInterface.registerListener(this); + + let mobileConnection = gMobileConnectionService.getItemByServiceId(aClientId); + mobileConnection.registerListener(this); + + this._dataInfo = { + state: mobileConnection.data.state, + type: mobileConnection.data.type, + roaming: mobileConnection.data.roaming + } +} +DataCallHandler.prototype = { + classID: DATACALLHANDLER_CID, + classInfo: XPCOMUtils.generateCI({classID: DATACALLHANDLER_CID, + classDescription: "Data Call Handler", + interfaces: [Ci.nsIDataCallHandler]}), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallHandler, + Ci.nsIDataCallInterfaceListener, + Ci.nsIMobileConnectionListener]), + + clientId: 0, + dataCallInterface: null, + dataCallSettings: null, + dataNetworkInterfaces: null, + _dataCalls: null, + _dataInfo: null, + + // Apn settings to be setup after data call are cleared. + _pendingApnSettings: null, + + debug: function(aMsg) { + dump("-*- DataCallHandler[" + this.clientId + "]: " + aMsg + "\n"); + }, + + shutdown: function() { + // Shutdown all RIL network interfaces + this.dataNetworkInterfaces.forEach(function(networkInterface) { + gNetworkManager.unregisterNetworkInterface(networkInterface); + networkInterface.shutdown(); + networkInterface = null; + }); + this.dataNetworkInterfaces.clear(); + this._dataCalls = []; + this.clientId = null; + + this.dataCallInterface.unregisterListener(this); + this.dataCallInterface = null; + + let mobileConnection = + gMobileConnectionService.getItemByServiceId(this.clientId); + mobileConnection.unregisterListener(this); + }, + + /** + * Check if we get all necessary APN data. + */ + _validateApnSetting: function(aApnSetting) { + return (aApnSetting && + aApnSetting.apn && + aApnSetting.types && + aApnSetting.types.length); + }, + + _convertApnType: function(aApnType) { + switch (aApnType) { + case "default": + return NETWORK_TYPE_MOBILE; + case "mms": + return NETWORK_TYPE_MOBILE_MMS; + case "supl": + return NETWORK_TYPE_MOBILE_SUPL; + case "ims": + return NETWORK_TYPE_MOBILE_IMS; + case "dun": + return NETWORK_TYPE_MOBILE_DUN; + case "fota": + return NETWORK_TYPE_MOBILE_FOTA; + default: + return NETWORK_TYPE_UNKNOWN; + } + }, + + _compareDataCallOptions: function(aDataCall, aNewDataCall) { + return aDataCall.apnProfile.apn == aNewDataCall.apnProfile.apn && + aDataCall.apnProfile.user == aNewDataCall.apnProfile.user && + aDataCall.apnProfile.password == aNewDataCall.apnProfile.passwd && + aDataCall.apnProfile.authType == aNewDataCall.apnProfile.authType && + aDataCall.apnProfile.protocol == aNewDataCall.apnProfile.protocol && + aDataCall.apnProfile.roaming_protocol == aNewDataCall.apnProfile.roaming_protocol; + }, + + /** + * This function will do the following steps: + * 1. Clear the cached APN settings in the RIL. + * 2. Combine APN, user name, and password as the key of |byApn| object to + * refer to the corresponding APN setting. + * 3. Use APN type as the index of |byType| object to refer to the + * corresponding APN setting. + * 4. Create RilNetworkInterface for each APN setting created at step 2. + */ + _setupApnSettings: function(aNewApnSettings) { + if (!aNewApnSettings) { + return; + } + if (DEBUG) this.debug("setupApnSettings: " + JSON.stringify(aNewApnSettings)); + + // Shutdown all network interfaces and clear data calls. + this.dataNetworkInterfaces.forEach(function(networkInterface) { + gNetworkManager.unregisterNetworkInterface(networkInterface); + networkInterface.shutdown(); + networkInterface = null; + }); + this.dataNetworkInterfaces.clear(); + this._dataCalls = []; + + // Cache the APN settings by APNs and by types in the RIL. + for (let inputApnSetting of aNewApnSettings) { + if (!this._validateApnSetting(inputApnSetting)) { + continue; + } + + // Use APN type as the key of dataNetworkInterfaces to refer to the + // corresponding RILNetworkInterface. + for (let i = 0; i < inputApnSetting.types.length; i++) { + let apnType = inputApnSetting.types[i]; + let networkType = this._convertApnType(apnType); + if (networkType === NETWORK_TYPE_UNKNOWN) { + if (DEBUG) this.debug("Invalid apn type: " + apnType); + continue; + } + + if (DEBUG) this.debug("Preparing RILNetworkInterface for type: " + apnType); + // Create DataCall for RILNetworkInterface or reuse one that is shareable. + let dataCall; + for (let i = 0; i < this._dataCalls.length; i++) { + if (this._dataCalls[i].canHandleApn(inputApnSetting)) { + if (DEBUG) this.debug("Found shareable DataCall, reusing it."); + dataCall = this._dataCalls[i]; + break; + } + } + + if (!dataCall) { + if (DEBUG) this.debug("No shareable DataCall found, creating one."); + dataCall = new DataCall(this.clientId, inputApnSetting, this); + this._dataCalls.push(dataCall); + } + + try { + let networkInterface = new RILNetworkInterface(this, networkType, + inputApnSetting, + dataCall); + gNetworkManager.registerNetworkInterface(networkInterface); + this.dataNetworkInterfaces.set(networkType, networkInterface); + } catch (e) { + if (DEBUG) { + this.debug("Error setting up RILNetworkInterface for type " + + apnType + ": " + e); + } + } + } + } + }, + + /** + * Check if all data is disconnected. + */ + allDataDisconnected: function() { + for (let i = 0; i < this._dataCalls.length; i++) { + let dataCall = this._dataCalls[i]; + if (dataCall.state != NETWORK_STATE_UNKNOWN && + dataCall.state != NETWORK_STATE_DISCONNECTED) { + return false; + } + } + return true; + }, + + deactivateDataCallsAndWait: function() { + return new Promise((aResolve, aReject) => { + this.deactivateDataCalls({ + notifyDataCallsDisconnected: function() { + aResolve(); + } + }); + }); + }, + + updateApnSettings: function(aNewApnSettings) { + if (!aNewApnSettings) { + return; + } + if (this._pendingApnSettings) { + // Change of apn settings in process, just update to the newest. + this._pengingApnSettings = aNewApnSettings; + return; + } + + this._pendingApnSettings = aNewApnSettings; + this.deactivateDataCallsAndWait().then(() => { + this._setupApnSettings(this._pendingApnSettings); + this._pendingApnSettings = null; + this.updateRILNetworkInterface(); + }); + }, + + updateRILNetworkInterface: function() { + let networkInterface = this.dataNetworkInterfaces.get(NETWORK_TYPE_MOBILE); + if (!networkInterface) { + if (DEBUG) { + this.debug("No network interface for default data."); + } + return; + } + + let connection = + gMobileConnectionService.getItemByServiceId(this.clientId); + + // This check avoids data call connection if the radio is not ready + // yet after toggling off airplane mode. + let radioState = connection && connection.radioState; + if (radioState != Ci.nsIMobileConnection.MOBILE_RADIO_STATE_ENABLED) { + if (DEBUG) { + this.debug("RIL is not ready for data connection: radio's not ready"); + } + return; + } + + // We only watch at "ril.data.enabled" flag changes for connecting or + // disconnecting the data call. If the value of "ril.data.enabled" is + // true and any of the remaining flags change the setting application + // should turn this flag to false and then to true in order to reload + // the new values and reconnect the data call. + if (this.dataCallSettings.oldEnabled === this.dataCallSettings.enabled) { + if (DEBUG) { + this.debug("No changes for ril.data.enabled flag. Nothing to do."); + } + return; + } + + let dataInfo = connection && connection.data; + let isRegistered = + dataInfo && + dataInfo.state == RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED; + let haveDataConnection = + dataInfo && + dataInfo.type != RIL.GECKO_MOBILE_CONNECTION_STATE_UNKNOWN; + if (!isRegistered || !haveDataConnection) { + if (DEBUG) { + this.debug("RIL is not ready for data connection: Phone's not " + + "registered or doesn't have data connection."); + } + return; + } + let wifi_active = false; + if (gNetworkManager.activeNetworkInfo && + gNetworkManager.activeNetworkInfo.type == NETWORK_TYPE_WIFI) { + wifi_active = true; + } + + let defaultDataCallConnected = networkInterface.connected; + + // We have moved part of the decision making into DataCall, the rest will be + // moved after Bug 904514 - [meta] NetworkManager enhancement. + if (networkInterface.enabled && + (!this.dataCallSettings.enabled || + (dataInfo.roaming && !this.dataCallSettings.roamingEnabled))) { + if (DEBUG) { + this.debug("Data call settings: disconnect data call."); + } + networkInterface.disconnect(); + return; + } + + if (networkInterface.enabled && wifi_active) { + if (DEBUG) { + this.debug("Disconnect data call when Wifi is connected."); + } + networkInterface.disconnect(); + return; + } + + if (!this.dataCallSettings.enabled || defaultDataCallConnected) { + if (DEBUG) { + this.debug("Data call settings: nothing to do."); + } + return; + } + if (dataInfo.roaming && !this.dataCallSettings.roamingEnabled) { + if (DEBUG) { + this.debug("We're roaming, but data roaming is disabled."); + } + return; + } + if (wifi_active) { + if (DEBUG) { + this.debug("Don't connect data call when Wifi is connected."); + } + return; + } + if (this._pendingApnSettings) { + if (DEBUG) this.debug("We're changing apn settings, ignore any changes."); + return; + } + + if (this._deactivatingDataCalls) { + if (DEBUG) this.debug("We're deactivating all data calls, ignore any changes."); + return; + } + + if (DEBUG) { + this.debug("Data call settings: connect data call."); + } + networkInterface.connect(); + }, + + _isMobileNetworkType: function(aNetworkType) { + if (aNetworkType === NETWORK_TYPE_MOBILE || + aNetworkType === NETWORK_TYPE_MOBILE_MMS || + aNetworkType === NETWORK_TYPE_MOBILE_SUPL || + aNetworkType === NETWORK_TYPE_MOBILE_IMS || + aNetworkType === NETWORK_TYPE_MOBILE_DUN || + aNetworkType === NETWORK_TYPE_MOBILE_FOTA) { + return true; + } + + return false; + }, + + getDataCallStateByType: function(aNetworkType) { + if (!this._isMobileNetworkType(aNetworkType)) { + if (DEBUG) this.debug(aNetworkType + " is not a mobile network type!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + + let networkInterface = this.dataNetworkInterfaces.get(aNetworkType); + if (!networkInterface) { + return NETWORK_STATE_UNKNOWN; + } + return networkInterface.info.state; + }, + + setupDataCallByType: function(aNetworkType) { + if (DEBUG) { + this.debug("setupDataCallByType: " + aNetworkType); + } + + if (!this._isMobileNetworkType(aNetworkType)) { + if (DEBUG) this.debug(aNetworkType + " is not a mobile network type!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + + let networkInterface = this.dataNetworkInterfaces.get(aNetworkType); + if (!networkInterface) { + if (DEBUG) { + this.debug("No network interface for type: " + aNetworkType); + } + return; + } + + networkInterface.connect(); + }, + + deactivateDataCallByType: function(aNetworkType) { + if (DEBUG) { + this.debug("deactivateDataCallByType: " + aNetworkType); + } + + if (!this._isMobileNetworkType(aNetworkType)) { + if (DEBUG) this.debug(aNetworkType + " is not a mobile network type!"); + throw Cr.NS_ERROR_INVALID_ARG; + } + + let networkInterface = this.dataNetworkInterfaces.get(aNetworkType); + if (!networkInterface) { + if (DEBUG) { + this.debug("No network interface for type: " + aNetworkType); + } + return; + } + + networkInterface.disconnect(); + }, + + _deactivatingDataCalls: false, + + deactivateDataCalls: function(aCallback) { + let dataDisconnecting = false; + this.dataNetworkInterfaces.forEach(function(networkInterface) { + if (networkInterface.enabled) { + if (networkInterface.info.state != NETWORK_STATE_UNKNOWN && + networkInterface.info.state != NETWORK_STATE_DISCONNECTED) { + dataDisconnecting = true; + } + networkInterface.disconnect(); + } + }); + + this._deactivatingDataCalls = dataDisconnecting; + if (!dataDisconnecting) { + aCallback.notifyDataCallsDisconnected(); + return; + } + + let callback = { + notifyAllDataDisconnected: () => { + this._unregisterListener(callback); + aCallback.notifyDataCallsDisconnected(); + } + }; + this._registerListener(callback); + }, + + _listeners: null, + + _notifyListeners: function(aMethodName, aArgs) { + let listeners = this._listeners.slice(); + for (let listener of listeners) { + if (this._listeners.indexOf(listener) == -1) { + // Listener has been unregistered in previous run. + continue; + } + + let handler = listener[aMethodName]; + try { + handler.apply(listener, aArgs); + } catch (e) { + this.debug("listener for " + aMethodName + " threw an exception: " + e); + } + } + }, + + _registerListener: function(aListener) { + if (this._listeners.indexOf(aListener) >= 0) { + return; + } + + this._listeners.push(aListener); + }, + + _unregisterListener: function(aListener) { + let index = this._listeners.indexOf(aListener); + if (index >= 0) { + this._listeners.splice(index, 1); + } + }, + + _findDataCallByCid: function(aCid) { + if (aCid === undefined || aCid < 0) { + return -1; + } + + for (let i = 0; i < this._dataCalls.length; i++) { + let datacall = this._dataCalls[i]; + if (datacall.linkInfo.cid != null && + datacall.linkInfo.cid == aCid) { + return i; + } + } + + return -1; + }, + + /** + * Notify about data call setup error, called from DataCall. + */ + notifyDataCallError: function(aDataCall, aErrorMsg) { + // Notify data call error only for data APN + let networkInterface = this.dataNetworkInterfaces.get(NETWORK_TYPE_MOBILE); + if (networkInterface && networkInterface.enabled) { + let dataCall = networkInterface.dataCall; + if (this._compareDataCallOptions(dataCall, aDataCall)) { + Services.obs.notifyObservers(networkInterface.info, + TOPIC_DATA_CALL_ERROR, aErrorMsg); + } + } + }, + + /** + * Notify about data call changed, called from DataCall. + */ + notifyDataCallChanged: function(aUpdatedDataCall) { + // Process pending radio power off request after all data calls + // are disconnected. + if (aUpdatedDataCall.state == NETWORK_STATE_DISCONNECTED || + aUpdatedDataCall.state == NETWORK_STATE_UNKNOWN && + this.allDataDisconnected() && this._deactivatingDataCalls) { + this._deactivatingDataCalls = false; + this._notifyListeners("notifyAllDataDisconnected", { + clientId: this.clientId + }); + } + }, + + // nsIDataCallInterfaceListener + + notifyDataCallListChanged: function(aCount, aDataCallList) { + let currentDataCalls = this._dataCalls.slice(); + for (let i = 0; i < aDataCallList.length; i++) { + let dataCall = aDataCallList[i]; + let index = this._findDataCallByCid(dataCall.cid); + if (index == -1) { + if (DEBUG) { + this.debug("Unexpected new data call: " + JSON.stringify(dataCall)); + } + continue; + } + currentDataCalls[index].onDataCallChanged(dataCall); + currentDataCalls[index] = null; + } + + // If there is any CONNECTED DataCall left in currentDataCalls, means that + // it is missing in dataCallList, we should send a DISCONNECTED event to + // notify about this. + for (let i = 0; i < currentDataCalls.length; i++) { + let currentDataCall = currentDataCalls[i]; + if (currentDataCall && currentDataCall.linkInfo.cid != null && + currentDataCall.state == NETWORK_STATE_CONNECTED) { + if (DEBUG) { + this.debug("Expected data call missing: " + JSON.stringify( + currentDataCall.apnProfile) + ", must have been DISCONNECTED."); + } + currentDataCall.onDataCallChanged({ + state: NETWORK_STATE_DISCONNECTED + }); + } + } + }, + + // nsIMobileConnectionListener + + notifyVoiceChanged: function() {}, + + notifyDataChanged: function () { + let connection = gMobileConnectionService.getItemByServiceId(this.clientId); + let newDataInfo = connection.data; + + if (this._dataInfo.state == newDataInfo.state && + this._dataInfo.type == newDataInfo.type && + this._dataInfo.roaming == newDataInfo.roaming) { + return; + } + + this._dataInfo.state = newDataInfo.state; + this._dataInfo.type = newDataInfo.type; + this._dataInfo.roaming = newDataInfo.roaming; + this.updateRILNetworkInterface(); + }, + + notifyDataError: function (aMessage) {}, + + notifyCFStateChanged: function(aAction, aReason, aNumber, aTimeSeconds, aServiceClass) {}, + + notifyEmergencyCbModeChanged: function(aActive, aTimeoutMs) {}, + + notifyOtaStatusChanged: function(aStatus) {}, + + notifyRadioStateChanged: function() {}, + + notifyClirModeChanged: function(aMode) {}, + + notifyLastKnownNetworkChanged: function() {}, + + notifyLastKnownHomeNetworkChanged: function() {}, + + notifyNetworkSelectionModeChanged: function() {}, + + notifyDeviceIdentitiesChanged: function() {} +}; + +function DataCall(aClientId, aApnSetting, aDataCallHandler) { + this.clientId = aClientId; + this.dataCallHandler = aDataCallHandler; + this.apnProfile = { + apn: aApnSetting.apn, + user: aApnSetting.user, + password: aApnSetting.password, + authType: aApnSetting.authtype, + protocol: aApnSetting.protocol, + roaming_protocol: aApnSetting.roaming_protocol + }; + this.linkInfo = { + cid: null, + ifname: null, + addresses: [], + dnses: [], + gateways: [], + pcscf: [], + mtu: null + }; + this.state = NETWORK_STATE_UNKNOWN; + this.requestedNetworkIfaces = []; +} +DataCall.prototype = { + /** + * Standard values for the APN connection retry process + * Retry funcion: time(secs) = A * numer_of_retries^2 + B + */ + NETWORK_APNRETRY_FACTOR: 8, + NETWORK_APNRETRY_ORIGIN: 3, + NETWORK_APNRETRY_MAXRETRIES: 10, + + dataCallHandler: null, + + // Event timer for connection retries + timer: null, + + // APN failed connections. Retry counter + apnRetryCounter: 0, + + // Array to hold RILNetworkInterfaces that requested this DataCall. + requestedNetworkIfaces: null, + + /** + * @return "deactivate" if <ifname> changes or one of the aCurrentDataCall + * addresses is missing in updatedDataCall, or "identical" if no + * changes found, or "changed" otherwise. + */ + _compareDataCallLink: function(aUpdatedDataCall, aCurrentDataCall) { + // If network interface is changed, report as "deactivate". + if (aUpdatedDataCall.ifname != aCurrentDataCall.ifname) { + return "deactivate"; + } + + // If any existing address is missing, report as "deactivate". + for (let i = 0; i < aCurrentDataCall.addresses.length; i++) { + let address = aCurrentDataCall.addresses[i]; + if (aUpdatedDataCall.addresses.indexOf(address) < 0) { + return "deactivate"; + } + } + + if (aCurrentDataCall.addresses.length != aUpdatedDataCall.addresses.length) { + // Since now all |aCurrentDataCall.addresses| are found in + // |aUpdatedDataCall.addresses|, this means one or more new addresses are + // reported. + return "changed"; + } + + let fields = ["gateways", "dnses"]; + for (let i = 0; i < fields.length; i++) { + // Compare <datacall>.<field>. + let field = fields[i]; + let lhs = aUpdatedDataCall[field], rhs = aCurrentDataCall[field]; + if (lhs.length != rhs.length) { + return "changed"; + } + for (let i = 0; i < lhs.length; i++) { + if (lhs[i] != rhs[i]) { + return "changed"; + } + } + } + + if (aCurrentDataCall.mtu != aUpdatedDataCall.mtu) { + return "changed"; + } + + return "identical"; + }, + + _getGeckoDataCallState:function (aDataCall) { + if (aDataCall.active == Ci.nsIDataCallInterface.DATACALL_STATE_ACTIVE_UP || + aDataCall.active == Ci.nsIDataCallInterface.DATACALL_STATE_ACTIVE_DOWN) { + return NETWORK_STATE_CONNECTED; + } + + return NETWORK_STATE_DISCONNECTED; + }, + + onSetupDataCallResult: function(aDataCall) { + this.debug("onSetupDataCallResult: " + JSON.stringify(aDataCall)); + let errorMsg = aDataCall.errorMsg; + if (aDataCall.failCause && + aDataCall.failCause != Ci.nsIDataCallInterface.DATACALL_FAIL_NONE) { + errorMsg = + RIL.RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[aDataCall.failCause]; + } + + if (errorMsg) { + if (DEBUG) { + this.debug("SetupDataCall error for apn " + this.apnProfile.apn + ": " + + errorMsg + " (" + aDataCall.failCause + "), retry time: " + + aDataCall.suggestedRetryTime); + } + + this.state = NETWORK_STATE_DISCONNECTED; + + if (this.requestedNetworkIfaces.length === 0) { + if (DEBUG) this.debug("This DataCall is not requested anymore."); + return; + } + + // Let DataCallHandler notify MobileConnectionService + this.dataCallHandler.notifyDataCallError(this, errorMsg); + + // For suggestedRetryTime, the value of INT32_MAX(0x7fffffff) means no retry. + if (aDataCall.suggestedRetryTime === INT32_MAX || + this.isPermanentFail(aDataCall.failCause, errorMsg)) { + if (DEBUG) this.debug("Data call error: no retry needed."); + return; + } + + this.retry(aDataCall.suggestedRetryTime); + return; + } + + this.apnRetryCounter = 0; + this.linkInfo.cid = aDataCall.cid; + + if (this.requestedNetworkIfaces.length === 0) { + if (DEBUG) { + this.debug("State is connected, but no network interface requested" + + " this DataCall"); + } + this.deactivate(); + return; + } + + this.linkInfo.ifname = aDataCall.ifname; + this.linkInfo.addresses = aDataCall.addresses ? aDataCall.addresses.split(" ") : []; + this.linkInfo.gateways = aDataCall.gateways ? aDataCall.gateways.split(" ") : []; + this.linkInfo.dnses = aDataCall.dnses ? aDataCall.dnses.split(" ") : []; + this.linkInfo.pcscf = aDataCall.pcscf ? aDataCall.pcscf.split(" ") : []; + this.linkInfo.mtu = aDataCall.mtu > 0 ? aDataCall.mtu : 0; + this.state = this._getGeckoDataCallState(aDataCall); + + // Notify DataCallHandler about data call connected. + this.dataCallHandler.notifyDataCallChanged(this); + + for (let i = 0; i < this.requestedNetworkIfaces.length; i++) { + this.requestedNetworkIfaces[i].notifyRILNetworkInterface(); + } + }, + + onDeactivateDataCallResult: function() { + if (DEBUG) this.debug("onDeactivateDataCallResult"); + + this.reset(); + + if (this.requestedNetworkIfaces.length > 0) { + if (DEBUG) { + this.debug("State is disconnected/unknown, but this DataCall is" + + " requested."); + } + this.setup(); + return; + } + + // Notify DataCallHandler about data call disconnected. + this.dataCallHandler.notifyDataCallChanged(this); + }, + + onDataCallChanged: function(aUpdatedDataCall) { + if (DEBUG) { + this.debug("onDataCallChanged: " + JSON.stringify(aUpdatedDataCall)); + } + + if (this.state == NETWORK_STATE_CONNECTING || + this.state == NETWORK_STATE_DISCONNECTING) { + if (DEBUG) { + this.debug("We are in connecting/disconnecting state, ignore any " + + "unsolicited event for now."); + } + return; + } + + let dataCallState = this._getGeckoDataCallState(aUpdatedDataCall); + if (this.state == dataCallState && + dataCallState != NETWORK_STATE_CONNECTED) { + return; + } + + let newLinkInfo = { + ifname: aUpdatedDataCall.ifname, + addresses: aUpdatedDataCall.addresses ? aUpdatedDataCall.addresses.split(" ") : [], + dnses: aUpdatedDataCall.dnses ? aUpdatedDataCall.dnses.split(" ") : [], + gateways: aUpdatedDataCall.gateways ? aUpdatedDataCall.gateways.split(" ") : [], + pcscf: aUpdatedDataCall.pcscf ? aUpdatedDataCall.pcscf.split(" ") : [], + mtu: aUpdatedDataCall.mtu > 0 ? aUpdatedDataCall.mtu : 0 + }; + + switch (dataCallState) { + case NETWORK_STATE_CONNECTED: + if (this.state == NETWORK_STATE_CONNECTED) { + let result = + this._compareDataCallLink(newLinkInfo, this.linkInfo); + + if (result == "identical") { + if (DEBUG) this.debug("No changes in data call."); + return; + } + if (result == "deactivate") { + if (DEBUG) this.debug("Data link changed, cleanup."); + this.deactivate(); + return; + } + // Minor change, just update and notify. + if (DEBUG) { + this.debug("Data link minor change, just update and notify."); + } + + this.linkInfo.addresses = newLinkInfo.addresses.slice(); + this.linkInfo.gateways = newLinkInfo.gateways.slice(); + this.linkInfo.dnses = newLinkInfo.dnses.slice(); + this.linkInfo.pcscf = newLinkInfo.pcscf.slice(); + this.linkInfo.mtu = newLinkInfo.mtu; + } + break; + case NETWORK_STATE_DISCONNECTED: + case NETWORK_STATE_UNKNOWN: + if (this.state == NETWORK_STATE_CONNECTED) { + // Notify first on unexpected data call disconnection. + this.state = dataCallState; + for (let i = 0; i < this.requestedNetworkIfaces.length; i++) { + this.requestedNetworkIfaces[i].notifyRILNetworkInterface(); + } + } + this.reset(); + + if (this.requestedNetworkIfaces.length > 0) { + if (DEBUG) { + this.debug("State is disconnected/unknown, but this DataCall is" + + " requested."); + } + this.setup(); + return; + } + break; + } + + this.state = dataCallState; + + // Notify DataCallHandler about data call changed. + this.dataCallHandler.notifyDataCallChanged(this); + + for (let i = 0; i < this.requestedNetworkIfaces.length; i++) { + this.requestedNetworkIfaces[i].notifyRILNetworkInterface(); + } + }, + + // Helpers + + debug: function(aMsg) { + dump("-*- DataCall[" + this.clientId + ":" + this.apnProfile.apn + "]: " + + aMsg + "\n"); + }, + + get connected() { + return this.state == NETWORK_STATE_CONNECTED; + }, + + isPermanentFail: function(aDataFailCause, aErrorMsg) { + // Check ril.h for 'no retry' data call fail causes. + if (aErrorMsg === RIL.GECKO_ERROR_RADIO_NOT_AVAILABLE || + aErrorMsg === RIL.GECKO_ERROR_INVALID_PARAMETER || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_OPERATOR_BARRED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_MISSING_UKNOWN_APN || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_UNKNOWN_PDP_ADDRESS_TYPE || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_USER_AUTHENTICATION || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_ACTIVATION_REJECT_GGSN || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_SERVICE_OPTION_NOT_SUPPORTED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_SERVICE_OPTION_NOT_SUBSCRIBED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_NSAPI_IN_USE || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_ONLY_IPV4_ALLOWED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_ONLY_IPV6_ALLOWED || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_PROTOCOL_ERRORS || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_RADIO_POWER_OFF || + aDataFailCause === Ci.nsIDataCallInterface.DATACALL_FAIL_TETHERED_CALL_ACTIVE) { + return true; + } + + return false; + }, + + inRequestedTypes: function(aType) { + for (let i = 0; i < this.requestedNetworkIfaces.length; i++) { + if (this.requestedNetworkIfaces[i].info.type == aType) { + return true; + } + } + return false; + }, + + canHandleApn: function(aApnSetting) { + let isIdentical = this.apnProfile.apn == aApnSetting.apn && + (this.apnProfile.user || '') == (aApnSetting.user || '') && + (this.apnProfile.password || '') == (aApnSetting.password || '') && + (this.apnProfile.authType || '') == (aApnSetting.authtype || ''); + + if (RILQUIRKS_HAVE_IPV6) { + isIdentical = isIdentical && + (this.apnProfile.protocol || '') == (aApnSetting.protocol || '') && + (this.apnProfile.roaming_protocol || '') == (aApnSetting.roaming_protocol || ''); + } + + return isIdentical; + }, + + resetLinkInfo: function() { + this.linkInfo.cid = null; + this.linkInfo.ifname = null; + this.linkInfo.addresses = []; + this.linkInfo.dnses = []; + this.linkInfo.gateways = []; + this.linkInfo.pcscf = []; + this.linkInfo.mtu = null; + }, + + reset: function() { + this.resetLinkInfo(); + + this.state = NETWORK_STATE_UNKNOWN; + }, + + connect: function(aNetworkInterface) { + if (DEBUG) this.debug("connect: " + aNetworkInterface.info.type); + + if (this.requestedNetworkIfaces.indexOf(aNetworkInterface) == -1) { + this.requestedNetworkIfaces.push(aNetworkInterface); + } + + if (this.state == NETWORK_STATE_CONNECTING || + this.state == NETWORK_STATE_DISCONNECTING) { + return; + } + if (this.state == NETWORK_STATE_CONNECTED) { + // This needs to run asynchronously, to behave the same way as the case of + // non-shared apn, see bug 1059110. + Services.tm.currentThread.dispatch(() => { + // Do not notify if state changed while this event was being dispatched, + // the state probably was notified already or need not to be notified. + if (aNetworkInterface.info.state == RIL.GECKO_NETWORK_STATE_CONNECTED) { + aNetworkInterface.notifyRILNetworkInterface(); + } + }, Ci.nsIEventTarget.DISPATCH_NORMAL); + return; + } + + // If retry mechanism is running on background, stop it since we are going + // to setup data call now. + if (this.timer) { + this.timer.cancel(); + } + + this.setup(); + }, + + setup: function() { + if (DEBUG) { + this.debug("Going to set up data connection with APN " + + this.apnProfile.apn); + } + + let connection = + gMobileConnectionService.getItemByServiceId(this.clientId); + let dataInfo = connection && connection.data; + if (dataInfo == null || + dataInfo.state != RIL.GECKO_MOBILE_CONNECTION_STATE_REGISTERED || + dataInfo.type == RIL.GECKO_MOBILE_CONNECTION_STATE_UNKNOWN) { + return; + } + + let radioTechType = dataInfo.type; + let radioTechnology = RIL.GECKO_RADIO_TECH.indexOf(radioTechType); + let authType = RIL.RIL_DATACALL_AUTH_TO_GECKO.indexOf(this.apnProfile.authType); + // Use the default authType if the value in database is invalid. + // For the case that user might not select the authentication type. + if (authType == -1) { + if (DEBUG) { + this.debug("Invalid authType '" + this.apnProfile.authtype + + "', using '" + RIL.GECKO_DATACALL_AUTH_DEFAULT + "'"); + } + authType = RIL.RIL_DATACALL_AUTH_TO_GECKO.indexOf(RIL.GECKO_DATACALL_AUTH_DEFAULT); + } + + let pdpType = Ci.nsIDataCallInterface.DATACALL_PDP_TYPE_IPV4; + if (RILQUIRKS_HAVE_IPV6) { + pdpType = !dataInfo.roaming + ? RIL.RIL_DATACALL_PDP_TYPES.indexOf(this.apnProfile.protocol) + : RIL.RIL_DATACALL_PDP_TYPES.indexOf(this.apnProfile.roaming_protocol); + if (pdpType == -1) { + if (DEBUG) { + this.debug("Invalid pdpType '" + (!dataInfo.roaming + ? this.apnProfile.protocol + : this.apnProfile.roaming_protocol) + + "', using '" + RIL.GECKO_DATACALL_PDP_TYPE_DEFAULT + "'"); + } + pdpType = RIL.RIL_DATACALL_PDP_TYPES.indexOf(RIL.GECKO_DATACALL_PDP_TYPE_DEFAULT); + } + } + + let dcInterface = this.dataCallHandler.dataCallInterface; + dcInterface.setupDataCall( + this.apnProfile.apn, this.apnProfile.user, this.apnProfile.password, + authType, pdpType, { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallCallback]), + notifySetupDataCallSuccess: (aDataCall) => { + this.onSetupDataCallResult(aDataCall); + }, + notifyError: (aErrorMsg) => { + this.onSetupDataCallResult({errorMsg: aErrorMsg}); + } + }); + this.state = NETWORK_STATE_CONNECTING; + }, + + retry: function(aSuggestedRetryTime) { + let apnRetryTimer; + + // We will retry the connection in increasing times + // based on the function: time = A * numer_of_retries^2 + B + if (this.apnRetryCounter >= this.NETWORK_APNRETRY_MAXRETRIES) { + this.apnRetryCounter = 0; + this.timer = null; + if (DEBUG) this.debug("Too many APN Connection retries - STOP retrying"); + return; + } + + // If there is a valid aSuggestedRetryTime, override the retry timer. + if (aSuggestedRetryTime !== undefined && aSuggestedRetryTime >= 0) { + apnRetryTimer = aSuggestedRetryTime / 1000; + } else { + apnRetryTimer = this.NETWORK_APNRETRY_FACTOR * + (this.apnRetryCounter * this.apnRetryCounter) + + this.NETWORK_APNRETRY_ORIGIN; + } + this.apnRetryCounter++; + if (DEBUG) { + this.debug("Data call - APN Connection Retry Timer (secs-counter): " + + apnRetryTimer + "-" + this.apnRetryCounter); + } + + if (this.timer == null) { + // Event timer for connection retries + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + this.timer.initWithCallback(this, apnRetryTimer * 1000, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + disconnect: function(aNetworkInterface) { + if (DEBUG) this.debug("disconnect: " + aNetworkInterface.info.type); + + let index = this.requestedNetworkIfaces.indexOf(aNetworkInterface); + if (index != -1) { + this.requestedNetworkIfaces.splice(index, 1); + + if (this.state == NETWORK_STATE_DISCONNECTED || + this.state == NETWORK_STATE_UNKNOWN) { + if (this.timer) { + this.timer.cancel(); + } + this.reset(); + return; + } + + // Notify the DISCONNECTED event immediately after network interface is + // removed from requestedNetworkIfaces, to make the DataCall, shared or + // not, to have the same behavior. + Services.tm.currentThread.dispatch(() => { + // Do not notify if state changed while this event was being dispatched, + // the state probably was notified already or need not to be notified. + if (aNetworkInterface.info.state == RIL.GECKO_NETWORK_STATE_DISCONNECTED) { + aNetworkInterface.notifyRILNetworkInterface(); + + // Clear link info after notifying NetworkManager. + if (this.requestedNetworkIfaces.length === 0) { + this.resetLinkInfo(); + } + } + }, Ci.nsIEventTarget.DISPATCH_NORMAL); + } + + // Only deactivate data call if no more network interface needs this + // DataCall and if state is CONNECTED, for other states, we simply remove + // the network interface from requestedNetworkIfaces. + if (this.requestedNetworkIfaces.length > 0 || + this.state != NETWORK_STATE_CONNECTED) { + return; + } + + this.deactivate(); + }, + + deactivate: function() { + let reason = Ci.nsIDataCallInterface.DATACALL_DEACTIVATE_NO_REASON; + if (DEBUG) { + this.debug("Going to disconnect data connection cid " + this.linkInfo.cid); + } + + let dcInterface = this.dataCallHandler.dataCallInterface; + dcInterface.deactivateDataCall(this.linkInfo.cid, reason, { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCallCallback]), + notifySuccess: () => { + this.onDeactivateDataCallResult(); + }, + notifyError: (aErrorMsg) => { + this.onDeactivateDataCallResult(); + } + }); + + this.state = NETWORK_STATE_DISCONNECTING; + }, + + // Entry method for timer events. Used to reconnect to a failed APN + notify: function(aTimer) { + this.setup(); + }, + + shutdown: function() { + if (this.timer) { + this.timer.cancel(); + this.timer = null; + } + } +}; + +function RILNetworkInfo(aClientId, aType, aNetworkInterface) +{ + this.serviceId = aClientId; + this.type = aType; + + this.networkInterface = aNetworkInterface; +} +RILNetworkInfo.prototype = { + classID: RILNETWORKINFO_CID, + classInfo: XPCOMUtils.generateCI({classID: RILNETWORKINFO_CID, + classDescription: "RILNetworkInfo", + interfaces: [Ci.nsINetworkInfo, + Ci.nsIRilNetworkInfo]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInfo, + Ci.nsIRilNetworkInfo]), + + networkInterface: null, + + getDataCall: function() { + return this.networkInterface.dataCall; + }, + + getApnSetting: function() { + return this.networkInterface.apnSetting; + }, + + debug: function(aMsg) { + dump("-*- RILNetworkInfo[" + this.serviceId + ":" + this.type + "]: " + + aMsg + "\n"); + }, + + /** + * nsINetworkInfo Implementation + */ + get state() { + let dataCall = this.getDataCall(); + if (!dataCall.inRequestedTypes(this.type)) { + return NETWORK_STATE_DISCONNECTED; + } + return dataCall.state; + }, + + type: null, + + get name() { + return this.getDataCall().linkInfo.ifname; + }, + + getAddresses: function(aIps, aPrefixLengths) { + let addresses = this.getDataCall().linkInfo.addresses; + + let ips = []; + let prefixLengths = []; + for (let i = 0; i < addresses.length; i++) { + let [ip, prefixLength] = addresses[i].split("/"); + ips.push(ip); + prefixLengths.push(prefixLength); + } + + aIps.value = ips.slice(); + aPrefixLengths.value = prefixLengths.slice(); + + return ips.length; + }, + + getGateways: function(aCount) { + let linkInfo = this.getDataCall().linkInfo; + + if (aCount) { + aCount.value = linkInfo.gateways.length; + } + + return linkInfo.gateways.slice(); + }, + + getDnses: function(aCount) { + let linkInfo = this.getDataCall().linkInfo; + + if (aCount) { + aCount.value = linkInfo.dnses.length; + } + + return linkInfo.dnses.slice(); + }, + + /** + * nsIRilNetworkInfo Implementation + */ + + serviceId: 0, + + get iccId() { + let icc = gIccService.getIccByServiceId(this.serviceId); + let iccInfo = icc && icc.iccInfo; + + return iccInfo && iccInfo.iccid; + }, + + get mmsc() { + if (this.type != NETWORK_TYPE_MOBILE_MMS) { + if (DEBUG) this.debug("Error! Only MMS network can get MMSC."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + return this.getApnSetting().mmsc || ""; + }, + + get mmsProxy() { + if (this.type != NETWORK_TYPE_MOBILE_MMS) { + if (DEBUG) this.debug("Error! Only MMS network can get MMS proxy."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + return this.getApnSetting().mmsproxy || ""; + }, + + get mmsPort() { + if (this.type != NETWORK_TYPE_MOBILE_MMS) { + if (DEBUG) this.debug("Error! Only MMS network can get MMS port."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + // Note: Port 0 is reserved, so we treat it as invalid as well. + // See http://www.iana.org/assignments/port-numbers + return this.getApnSetting().mmsport || -1; + }, + + getPcscf: function(aCount) { + if (this.type != NETWORK_TYPE_MOBILE_IMS) { + if (DEBUG) this.debug("Error! Only IMS network can get pcscf."); + throw Cr.NS_ERROR_UNEXPECTED; + } + + let linkInfo = this.getDataCall().linkInfo; + + if (aCount) { + aCount.value = linkInfo.pcscf.length; + } + return linkInfo.pcscf.slice(); + }, +}; + +function RILNetworkInterface(aDataCallHandler, aType, aApnSetting, aDataCall) { + if (!aDataCall) { + throw new Error("No dataCall for RILNetworkInterface: " + type); + } + + this.dataCallHandler = aDataCallHandler; + this.enabled = false; + this.dataCall = aDataCall; + this.apnSetting = aApnSetting; + + this.info = new RILNetworkInfo(aDataCallHandler.clientId, aType, this); +} + +RILNetworkInterface.prototype = { + classID: RILNETWORKINTERFACE_CID, + classInfo: XPCOMUtils.generateCI({classID: RILNETWORKINTERFACE_CID, + classDescription: "RILNetworkInterface", + interfaces: [Ci.nsINetworkInterface]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInterface]), + + // If this RILNetworkInterface type is enabled or not. + enabled: null, + + apnSetting: null, + + dataCall: null, + + /** + * nsINetworkInterface Implementation + */ + + info: null, + + get httpProxyHost() { + return this.apnSetting.proxy || ""; + }, + + get httpProxyPort() { + return this.apnSetting.port || ""; + }, + + get mtu() { + // Value provided by network has higher priority than apn settings. + return this.dataCall.linkInfo.mtu || this.apnSetting.mtu || -1; + }, + + // Helpers + + debug: function(aMsg) { + dump("-*- RILNetworkInterface[" + this.dataCallHandler.clientId + ":" + + this.info.type + "]: " + aMsg + "\n"); + }, + + get connected() { + return this.info.state == NETWORK_STATE_CONNECTED; + }, + + notifyRILNetworkInterface: function() { + if (DEBUG) { + this.debug("notifyRILNetworkInterface type: " + this.info.type + + ", state: " + this.info.state); + } + + gNetworkManager.updateNetworkInterface(this); + }, + + connect: function() { + this.enabled = true; + + this.dataCall.connect(this); + }, + + disconnect: function() { + if (!this.enabled) { + return; + } + this.enabled = false; + + this.dataCall.disconnect(this); + }, + + shutdown: function() { + this.dataCall.shutdown(); + this.dataCall = null; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataCallManager]);
\ No newline at end of file diff --git a/dom/system/gonk/DataCallManager.manifest b/dom/system/gonk/DataCallManager.manifest new file mode 100644 index 000000000..2a982415e --- /dev/null +++ b/dom/system/gonk/DataCallManager.manifest @@ -0,0 +1,4 @@ +# DataCallManager.js +component {35b9efa2-e42c-45ce-8210-0a13e6f4aadc} DataCallManager.js +contract @mozilla.org/datacall/manager;1 {35b9efa2-e42c-45ce-8210-0a13e6f4aadc} +category profile-after-change DataCallManager @mozilla.org/datacall/manager;1
\ No newline at end of file diff --git a/dom/system/gonk/GeolocationUtil.cpp b/dom/system/gonk/GeolocationUtil.cpp new file mode 100644 index 000000000..99d484a19 --- /dev/null +++ b/dom/system/gonk/GeolocationUtil.cpp @@ -0,0 +1,28 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GeolocationUtil.h" + +double CalculateDeltaInMeter(double aLat, double aLon, double aLastLat, double aLastLon) +{ + // Use spherical law of cosines to calculate difference + // Not quite as correct as the Haversine but simpler and cheaper + const double radsInDeg = M_PI / 180.0; + const double rNewLat = aLat * radsInDeg; + const double rNewLon = aLon * radsInDeg; + const double rOldLat = aLastLat * radsInDeg; + const double rOldLon = aLastLon * radsInDeg; + // WGS84 equatorial radius of earth = 6378137m + double cosDelta = (sin(rNewLat) * sin(rOldLat)) + + (cos(rNewLat) * cos(rOldLat) * cos(rOldLon - rNewLon)); + if (cosDelta > 1.0) { + cosDelta = 1.0; + } else if (cosDelta < -1.0) { + cosDelta = -1.0; + } + return acos(cosDelta) * 6378137; +} + diff --git a/dom/system/gonk/GeolocationUtil.h b/dom/system/gonk/GeolocationUtil.h new file mode 100644 index 000000000..fde337fb8 --- /dev/null +++ b/dom/system/gonk/GeolocationUtil.h @@ -0,0 +1,13 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GEOLOCATIONUTIL_H +#define GEOLOCATIONUTIL_H + +double CalculateDeltaInMeter(double aLat, double aLon, double aLastLat, double aLastLon); + +#endif + diff --git a/dom/system/gonk/GonkGPSGeolocationProvider.cpp b/dom/system/gonk/GonkGPSGeolocationProvider.cpp new file mode 100644 index 000000000..9ce6ce2e5 --- /dev/null +++ b/dom/system/gonk/GonkGPSGeolocationProvider.cpp @@ -0,0 +1,706 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +#include "GonkGPSGeolocationProvider.h" + +#include <cmath> +#include <pthread.h> +#include <hardware/gps.h> + +#include "base/task.h" +#include "GeolocationUtil.h" +#include "mozstumbler/MozStumbler.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "nsContentUtils.h" +#include "nsGeoPosition.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsINetworkInterface.h" +#include "nsIObserverService.h" +#include "nsJSUtils.h" +#include "nsPrintfCString.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "prtime.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" + +#ifdef AGPS_TYPE_INVALID +#define AGPS_HAVE_DUAL_APN +#endif + +#define FLUSH_AIDE_DATA 0 + +#undef LOG +#undef ERR +#undef DBG +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "GonkGPSGeolocationProvider", ## args) +#define ERR(args...) __android_log_print(ANDROID_LOG_ERROR, "GonkGPSGeolocationProvider", ## args) +#define DBG(args...) __android_log_print(ANDROID_LOG_DEBUG, "GonkGPSGeolocationProvider" , ## args) + +using namespace mozilla; +using namespace mozilla::dom; + +static const int kDefaultPeriod = 1000; // ms +static bool gDebug_isLoggingEnabled = false; +static bool gDebug_isGPSLocationIgnored = false; +static const char* kMozSettingsChangedTopic = "mozsettings-changed"; +// Both of these settings can be toggled in the Gaia Developer settings screen. +static const char* kSettingDebugEnabled = "geolocation.debugging.enabled"; +static const char* kSettingDebugGpsIgnored = "geolocation.debugging.gps-locations-ignored"; + +// While most methods of GonkGPSGeolocationProvider should only be +// called from main thread, we deliberately put the Init and ShutdownGPS +// methods off main thread to avoid blocking. +NS_IMPL_ISUPPORTS(GonkGPSGeolocationProvider, + nsIGeolocationProvider, + nsIObserver, + nsISettingsServiceCallback) + +/* static */ GonkGPSGeolocationProvider* GonkGPSGeolocationProvider::sSingleton = nullptr; +GpsCallbacks GonkGPSGeolocationProvider::mCallbacks; + + +void +GonkGPSGeolocationProvider::LocationCallback(GpsLocation* location) +{ + if (gDebug_isGPSLocationIgnored) { + return; + } + + class UpdateLocationEvent : public Runnable { + public: + UpdateLocationEvent(nsGeoPosition* aPosition) + : mPosition(aPosition) + {} + NS_IMETHOD Run() override { + RefPtr<GonkGPSGeolocationProvider> provider = + GonkGPSGeolocationProvider::GetSingleton(); + nsCOMPtr<nsIGeolocationUpdate> callback = provider->mLocationCallback; + provider->mLastGPSPosition = mPosition; + if (callback) { + callback->Update(mPosition); + } + return NS_OK; + } + private: + RefPtr<nsGeoPosition> mPosition; + }; + + MOZ_ASSERT(location); + + const float kImpossibleAccuracy_m = 0.001; + if (location->accuracy < kImpossibleAccuracy_m) { + return; + } + + RefPtr<nsGeoPosition> somewhere = new nsGeoPosition(location->latitude, + location->longitude, + location->altitude, + location->accuracy, + location->accuracy, + location->bearing, + location->speed, + PR_Now() / PR_USEC_PER_MSEC); + // Note above: Can't use location->timestamp as the time from the satellite is a + // minimum of 16 secs old (see http://leapsecond.com/java/gpsclock.htm). + // All code from this point on expects the gps location to be timestamped with the + // current time, most notably: the geolocation service which respects maximumAge + // set in the DOM JS. + + if (gDebug_isLoggingEnabled) { + DBG("geo: GPS got a fix (%f, %f). accuracy: %f", + location->latitude, + location->longitude, + location->accuracy); + } + + RefPtr<UpdateLocationEvent> event = new UpdateLocationEvent(somewhere); + NS_DispatchToMainThread(event); + +} + +class NotifyObserversGPSTask final : public Runnable +{ +public: + explicit NotifyObserversGPSTask(const char16_t* aData) + : mData(aData) + {} + NS_IMETHOD Run() override { + RefPtr<nsIGeolocationProvider> provider = + GonkGPSGeolocationProvider::GetSingleton(); + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + obsService->NotifyObservers(provider, "geolocation-device-events", mData); + return NS_OK; + } +private: + const char16_t* mData; +}; + +void +GonkGPSGeolocationProvider::StatusCallback(GpsStatus* status) +{ + const char* msgStream=0; + switch (status->status) { + case GPS_STATUS_NONE: + msgStream = "geo: GPS_STATUS_NONE\n"; + break; + case GPS_STATUS_SESSION_BEGIN: + msgStream = "geo: GPS_STATUS_SESSION_BEGIN\n"; + break; + case GPS_STATUS_SESSION_END: + msgStream = "geo: GPS_STATUS_SESSION_END\n"; + break; + case GPS_STATUS_ENGINE_ON: + msgStream = "geo: GPS_STATUS_ENGINE_ON\n"; + NS_DispatchToMainThread(new NotifyObserversGPSTask(u"GPSStarting")); + break; + case GPS_STATUS_ENGINE_OFF: + msgStream = "geo: GPS_STATUS_ENGINE_OFF\n"; + NS_DispatchToMainThread(new NotifyObserversGPSTask(u"GPSShutdown")); + break; + default: + msgStream = "geo: Unknown GPS status\n"; + break; + } + if (gDebug_isLoggingEnabled){ + DBG("%s", msgStream); + } +} + +void +GonkGPSGeolocationProvider::SvStatusCallback(GpsSvStatus* sv_info) +{ + if (gDebug_isLoggingEnabled) { + static int numSvs = 0; + static uint32_t numEphemeris = 0; + static uint32_t numAlmanac = 0; + static uint32_t numUsedInFix = 0; + + unsigned int i = 1; + uint32_t svAlmanacCount = 0; + for (i = 1; i > 0; i <<= 1) { + if (i & sv_info->almanac_mask) { + svAlmanacCount++; + } + } + + uint32_t svEphemerisCount = 0; + for (i = 1; i > 0; i <<= 1) { + if (i & sv_info->ephemeris_mask) { + svEphemerisCount++; + } + } + + uint32_t svUsedCount = 0; + for (i = 1; i > 0; i <<= 1) { + if (i & sv_info->used_in_fix_mask) { + svUsedCount++; + } + } + + // Log the message only if the the status changed. + if (sv_info->num_svs != numSvs || + svAlmanacCount != numAlmanac || + svEphemerisCount != numEphemeris || + svUsedCount != numUsedInFix) { + + LOG( + "geo: Number of SVs have (visibility, almanac, ephemeris): (%d, %d, %d)." + " %d of these SVs were used in fix.\n", + sv_info->num_svs, svAlmanacCount, svEphemerisCount, svUsedCount); + + numSvs = sv_info->num_svs; + numAlmanac = svAlmanacCount; + numEphemeris = svEphemerisCount; + numUsedInFix = svUsedCount; + } + } +} + +void +GonkGPSGeolocationProvider::NmeaCallback(GpsUtcTime timestamp, const char* nmea, int length) +{ + if (gDebug_isLoggingEnabled) { + DBG("NMEA: timestamp:\t%lld, length: %d, %s", timestamp, length, nmea); + } +} + +void +GonkGPSGeolocationProvider::SetCapabilitiesCallback(uint32_t capabilities) +{ + class UpdateCapabilitiesEvent : public Runnable { + public: + UpdateCapabilitiesEvent(uint32_t aCapabilities) + : mCapabilities(aCapabilities) + {} + NS_IMETHOD Run() override { + RefPtr<GonkGPSGeolocationProvider> provider = + GonkGPSGeolocationProvider::GetSingleton(); + + provider->mSupportsScheduling = mCapabilities & GPS_CAPABILITY_SCHEDULING; + provider->mSupportsSingleShot = mCapabilities & GPS_CAPABILITY_SINGLE_SHOT; +#ifdef GPS_CAPABILITY_ON_DEMAND_TIME + provider->mSupportsTimeInjection = mCapabilities & GPS_CAPABILITY_ON_DEMAND_TIME; +#endif + return NS_OK; + } + private: + uint32_t mCapabilities; + }; + + NS_DispatchToMainThread(new UpdateCapabilitiesEvent(capabilities)); +} + +void +GonkGPSGeolocationProvider::AcquireWakelockCallback() +{ +} + +void +GonkGPSGeolocationProvider::ReleaseWakelockCallback() +{ +} + +typedef void *(*pthread_func)(void *); + +/** Callback for creating a thread that can call into the JS codes. + */ +pthread_t +GonkGPSGeolocationProvider::CreateThreadCallback(const char* name, void (*start)(void *), void* arg) +{ + pthread_t thread; + pthread_attr_t attr; + + pthread_attr_init(&attr); + + /* Unfortunately pthread_create and the callback disagreed on what + * start function should return. + */ + pthread_create(&thread, &attr, reinterpret_cast<pthread_func>(start), arg); + + return thread; +} + +void +GonkGPSGeolocationProvider::RequestUtcTimeCallback() +{ +} + +GonkGPSGeolocationProvider::GonkGPSGeolocationProvider() + : mStarted(false) + , mSupportsScheduling(false) + , mObservingSettingsChange(false) + , mSupportsSingleShot(false) + , mSupportsTimeInjection(false) + , mGpsInterface(nullptr) +{ +} + +GonkGPSGeolocationProvider::~GonkGPSGeolocationProvider() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mStarted, "Must call Shutdown before destruction"); + + sSingleton = nullptr; +} + +already_AddRefed<GonkGPSGeolocationProvider> +GonkGPSGeolocationProvider::GetSingleton() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!sSingleton) + sSingleton = new GonkGPSGeolocationProvider(); + + RefPtr<GonkGPSGeolocationProvider> provider = sSingleton; + return provider.forget(); +} + +const GpsInterface* +GonkGPSGeolocationProvider::GetGPSInterface() +{ + hw_module_t* module; + + if (hw_get_module(GPS_HARDWARE_MODULE_ID, (hw_module_t const**)&module)) + return nullptr; + + hw_device_t* device; + if (module->methods->open(module, GPS_HARDWARE_MODULE_ID, &device)) + return nullptr; + + gps_device_t* gps_device = (gps_device_t *)device; + const GpsInterface* result = gps_device->get_gps_interface(gps_device); + + if (result->size != sizeof(GpsInterface)) { + return nullptr; + } + return result; +} + +void +GonkGPSGeolocationProvider::RequestSettingValue(const char* aKey) +{ + MOZ_ASSERT(aKey); + nsCOMPtr<nsISettingsService> ss = do_GetService("@mozilla.org/settingsService;1"); + if (!ss) { + MOZ_ASSERT(ss); + return; + } + + nsCOMPtr<nsISettingsServiceLock> lock; + nsresult rv = ss->CreateLock(nullptr, getter_AddRefs(lock)); + if (NS_FAILED(rv)) { + ERR("error while createLock setting '%s': %d\n", aKey, uint32_t(rv)); + return; + } + + rv = lock->Get(aKey, this); + if (NS_FAILED(rv)) { + ERR("error while get setting '%s': %d\n", aKey, uint32_t(rv)); + return; + } +} + +void +GonkGPSGeolocationProvider::InjectLocation(double latitude, + double longitude, + float accuracy) +{ + if (gDebug_isLoggingEnabled) { + DBG("injecting location (%f, %f) accuracy: %f", latitude, longitude, accuracy); + } + + MOZ_ASSERT(NS_IsMainThread()); + if (!mGpsInterface) { + return; + } + + mGpsInterface->inject_location(latitude, longitude, accuracy); +} + +void +GonkGPSGeolocationProvider::Init() +{ + // Must not be main thread. Some GPS driver's first init takes very long. + MOZ_ASSERT(!NS_IsMainThread()); + + mGpsInterface = GetGPSInterface(); + if (!mGpsInterface) { + return; + } + + if (!mCallbacks.size) { + mCallbacks.size = sizeof(GpsCallbacks); + mCallbacks.location_cb = LocationCallback; + mCallbacks.status_cb = StatusCallback; + mCallbacks.sv_status_cb = SvStatusCallback; + mCallbacks.nmea_cb = NmeaCallback; + mCallbacks.set_capabilities_cb = SetCapabilitiesCallback; + mCallbacks.acquire_wakelock_cb = AcquireWakelockCallback; + mCallbacks.release_wakelock_cb = ReleaseWakelockCallback; + mCallbacks.create_thread_cb = CreateThreadCallback; + +#ifdef GPS_CAPABILITY_ON_DEMAND_TIME + mCallbacks.request_utc_time_cb = RequestUtcTimeCallback; +#endif + + } + + if (mGpsInterface->init(&mCallbacks) != 0) { + return; + } + + NS_DispatchToMainThread(NewRunnableMethod(this, &GonkGPSGeolocationProvider::StartGPS)); +} + +void +GonkGPSGeolocationProvider::StartGPS() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mGpsInterface); + + int32_t update = Preferences::GetInt("geo.default.update", kDefaultPeriod); + + int positionMode = GPS_POSITION_MODE_STANDALONE; + + if (!mSupportsScheduling) { + update = kDefaultPeriod; + } + + mGpsInterface->set_position_mode(positionMode, + GPS_POSITION_RECURRENCE_PERIODIC, + update, 0, 0); +#if FLUSH_AIDE_DATA + // Delete cached data + mGpsInterface->delete_aiding_data(GPS_DELETE_ALL); +#endif + + mGpsInterface->start(); +} + + +NS_IMPL_ISUPPORTS(GonkGPSGeolocationProvider::NetworkLocationUpdate, + nsIGeolocationUpdate) + +NS_IMETHODIMP +GonkGPSGeolocationProvider::NetworkLocationUpdate::Update(nsIDOMGeoPosition *position) +{ + RefPtr<GonkGPSGeolocationProvider> provider = + GonkGPSGeolocationProvider::GetSingleton(); + + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + position->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + + double lat, lon, acc; + coords->GetLatitude(&lat); + coords->GetLongitude(&lon); + coords->GetAccuracy(&acc); + + double delta = -1.0; + + static double sLastMLSPosLat = 0; + static double sLastMLSPosLon = 0; + + if (0 != sLastMLSPosLon || 0 != sLastMLSPosLat) { + delta = CalculateDeltaInMeter(lat, lon, sLastMLSPosLat, sLastMLSPosLon); + } + + sLastMLSPosLat = lat; + sLastMLSPosLon = lon; + + // if the MLS coord change is smaller than this arbitrarily small value + // assume the MLS coord is unchanged, and stick with the GPS location + const double kMinMLSCoordChangeInMeters = 10; + + DOMTimeStamp time_ms = 0; + if (provider->mLastGPSPosition) { + provider->mLastGPSPosition->GetTimestamp(&time_ms); + } + const int64_t diff_ms = (PR_Now() / PR_USEC_PER_MSEC) - time_ms; + + // We want to distinguish between the GPS being inactive completely + // and temporarily inactive. In the former case, we would use a low + // accuracy network location; in the latter, we only want a network + // location that appears to updating with movement. + + const bool isGPSFullyInactive = diff_ms > 1000 * 60 * 2; // two mins + const bool isGPSTempInactive = diff_ms > 1000 * 10; // 10 secs + + if (provider->mLocationCallback) { + if (isGPSFullyInactive || + (isGPSTempInactive && delta > kMinMLSCoordChangeInMeters)) + { + if (gDebug_isLoggingEnabled) { + DBG("Using MLS, GPS age:%fs, MLS Delta:%fm\n", diff_ms / 1000.0, delta); + } + provider->mLocationCallback->Update(position); + } else if (provider->mLastGPSPosition) { + if (gDebug_isLoggingEnabled) { + DBG("Using old GPS age:%fs\n", diff_ms / 1000.0); + } + + // This is a fallback case so that the GPS provider responds with its last + // location rather than waiting for a more recent GPS or network location. + // The service decides if the location is too old, not the provider. + provider->mLocationCallback->Update(provider->mLastGPSPosition); + } + } + provider->InjectLocation(lat, lon, acc); + return NS_OK; +} +NS_IMETHODIMP +GonkGPSGeolocationProvider::NetworkLocationUpdate::NotifyError(uint16_t error) +{ + return NS_OK; +} +NS_IMETHODIMP +GonkGPSGeolocationProvider::Startup() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (mStarted) { + return NS_OK; + } + + RequestSettingValue(kSettingDebugEnabled); + RequestSettingValue(kSettingDebugGpsIgnored); + + // Setup an observer to watch changes to the setting. + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + if (observerService) { + MOZ_ASSERT(!mObservingSettingsChange); + nsresult rv = observerService->AddObserver(this, kMozSettingsChangedTopic, false); + if (NS_FAILED(rv)) { + NS_WARNING("geo: Gonk GPS AddObserver failed"); + } else { + mObservingSettingsChange = true; + } + } + + if (!mInitThread) { + nsresult rv = NS_NewThread(getter_AddRefs(mInitThread)); + NS_ENSURE_SUCCESS(rv, rv); + } + + mInitThread->Dispatch(NewRunnableMethod(this, &GonkGPSGeolocationProvider::Init), + NS_DISPATCH_NORMAL); + + mNetworkLocationProvider = do_CreateInstance("@mozilla.org/geolocation/mls-provider;1"); + if (mNetworkLocationProvider) { + nsresult rv = mNetworkLocationProvider->Startup(); + if (NS_SUCCEEDED(rv)) { + RefPtr<NetworkLocationUpdate> update = new NetworkLocationUpdate(); + mNetworkLocationProvider->Watch(update); + } + } + + mStarted = true; + return NS_OK; +} + +NS_IMETHODIMP +GonkGPSGeolocationProvider::Watch(nsIGeolocationUpdate* aCallback) +{ + MOZ_ASSERT(NS_IsMainThread()); + + mLocationCallback = aCallback; + return NS_OK; +} + +NS_IMETHODIMP +GonkGPSGeolocationProvider::Shutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!mStarted) { + return NS_OK; + } + + mStarted = false; + if (mNetworkLocationProvider) { + mNetworkLocationProvider->Shutdown(); + mNetworkLocationProvider = nullptr; + } + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + nsresult rv; + rv = obs->RemoveObserver(this, kMozSettingsChangedTopic); + if (NS_FAILED(rv)) { + NS_WARNING("geo: Gonk GPS mozsettings RemoveObserver failed"); + } else { + mObservingSettingsChange = false; + } + } + + mInitThread->Dispatch(NewRunnableMethod(this, &GonkGPSGeolocationProvider::ShutdownGPS), + NS_DISPATCH_NORMAL); + + return NS_OK; +} + +void +GonkGPSGeolocationProvider::ShutdownGPS() +{ + MOZ_ASSERT(!mStarted, "Should only be called after Shutdown"); + + if (mGpsInterface) { + mGpsInterface->stop(); + mGpsInterface->cleanup(); + } +} + +NS_IMETHODIMP +GonkGPSGeolocationProvider::SetHighAccuracy(bool) +{ + return NS_OK; +} + +namespace { +int +ConvertToGpsNetworkType(int aNetworkInterfaceType) +{ + switch (aNetworkInterfaceType) { + case nsINetworkInfo::NETWORK_TYPE_WIFI: + return AGPS_RIL_NETWORK_TYPE_WIFI; + case nsINetworkInfo::NETWORK_TYPE_MOBILE: + return AGPS_RIL_NETWORK_TYPE_MOBILE; + case nsINetworkInfo::NETWORK_TYPE_MOBILE_MMS: + return AGPS_RIL_NETWORK_TYPE_MOBILE_MMS; + case nsINetworkInfo::NETWORK_TYPE_MOBILE_SUPL: + return AGPS_RIL_NETWORK_TYPE_MOBILE_SUPL; + case nsINetworkInfo::NETWORK_TYPE_MOBILE_DUN: + return AGPS_RIL_NETWORK_TTYPE_MOBILE_DUN; + default: + NS_WARNING(nsPrintfCString("Unknown network type mapping %d", + aNetworkInterfaceType).get()); + return -1; + } +} +} // namespace + +NS_IMETHODIMP +GonkGPSGeolocationProvider::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(aTopic, kMozSettingsChangedTopic)) { + // Read changed setting value + RootedDictionary<SettingChangeNotification> setting(RootingCx()); + if (!WrappedJSToDictionary(aSubject, setting)) { + return NS_OK; + } + + if (setting.mKey.EqualsASCII(kSettingDebugGpsIgnored)) { + LOG("received mozsettings-changed: ignoring\n"); + gDebug_isGPSLocationIgnored = + setting.mValue.isBoolean() ? setting.mValue.toBoolean() : false; + if (gDebug_isLoggingEnabled) { + DBG("GPS ignored %d\n", gDebug_isGPSLocationIgnored); + } + return NS_OK; + } else if (setting.mKey.EqualsASCII(kSettingDebugEnabled)) { + LOG("received mozsettings-changed: logging\n"); + gDebug_isLoggingEnabled = + setting.mValue.isBoolean() ? setting.mValue.toBoolean() : false; + return NS_OK; + } + } + + return NS_OK; +} + +/** nsISettingsServiceCallback **/ + +NS_IMETHODIMP +GonkGPSGeolocationProvider::Handle(const nsAString& aName, + JS::Handle<JS::Value> aResult) +{ + return NS_OK; +} + +NS_IMETHODIMP +GonkGPSGeolocationProvider::HandleError(const nsAString& aErrorMessage) +{ + return NS_OK; +} diff --git a/dom/system/gonk/GonkGPSGeolocationProvider.h b/dom/system/gonk/GonkGPSGeolocationProvider.h new file mode 100644 index 000000000..514398edf --- /dev/null +++ b/dom/system/gonk/GonkGPSGeolocationProvider.h @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +#ifndef GonkGPSGeolocationProvider_h +#define GonkGPSGeolocationProvider_h + +#include <hardware/gps.h> // for GpsInterface +#include "nsCOMPtr.h" +#include "nsIGeolocationProvider.h" +#include "nsIObserver.h" +#include "nsIDOMGeoPosition.h" +#include "nsISettingsService.h" + +class nsIThread; + +#define GONK_GPS_GEOLOCATION_PROVIDER_CID \ +{ 0x48525ec5, 0x5a7f, 0x490a, { 0x92, 0x77, 0xba, 0x66, 0xe0, 0xd2, 0x2c, 0x8b } } + +#define GONK_GPS_GEOLOCATION_PROVIDER_CONTRACTID \ +"@mozilla.org/gonk-gps-geolocation-provider;1" + +class GonkGPSGeolocationProvider : public nsIGeolocationProvider + , public nsIObserver + , public nsISettingsServiceCallback +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIGEOLOCATIONPROVIDER + NS_DECL_NSIOBSERVER + NS_DECL_NSISETTINGSSERVICECALLBACK + + static already_AddRefed<GonkGPSGeolocationProvider> GetSingleton(); + +private: + + /* Client should use GetSingleton() to get the provider instance. */ + GonkGPSGeolocationProvider(); + GonkGPSGeolocationProvider(const GonkGPSGeolocationProvider &); + GonkGPSGeolocationProvider & operator = (const GonkGPSGeolocationProvider &); + virtual ~GonkGPSGeolocationProvider(); + + static void LocationCallback(GpsLocation* location); + static void StatusCallback(GpsStatus* status); + static void SvStatusCallback(GpsSvStatus* sv_info); + static void NmeaCallback(GpsUtcTime timestamp, const char* nmea, int length); + static void SetCapabilitiesCallback(uint32_t capabilities); + static void AcquireWakelockCallback(); + static void ReleaseWakelockCallback(); + static pthread_t CreateThreadCallback(const char* name, void (*start)(void*), void* arg); + static void RequestUtcTimeCallback(); + + static GpsCallbacks mCallbacks; + + void Init(); + void StartGPS(); + void ShutdownGPS(); + void InjectLocation(double latitude, double longitude, float accuracy); + void RequestSettingValue(const char* aKey); + + const GpsInterface* GetGPSInterface(); + + static GonkGPSGeolocationProvider* sSingleton; + + bool mStarted; + + bool mSupportsScheduling; + bool mObservingSettingsChange; + bool mSupportsSingleShot; + bool mSupportsTimeInjection; + + const GpsInterface* mGpsInterface; + nsCOMPtr<nsIGeolocationUpdate> mLocationCallback; + nsCOMPtr<nsIThread> mInitThread; + nsCOMPtr<nsIGeolocationProvider> mNetworkLocationProvider; + nsCOMPtr<nsIDOMGeoPosition> mLastGPSPosition; + + class NetworkLocationUpdate : public nsIGeolocationUpdate + { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGEOLOCATIONUPDATE + + NetworkLocationUpdate() {} + + private: + virtual ~NetworkLocationUpdate() {} + }; +}; + +#endif /* GonkGPSGeolocationProvider_h */ diff --git a/dom/system/gonk/MozMtpCommon.h b/dom/system/gonk/MozMtpCommon.h new file mode 100644 index 000000000..81c0a3a74 --- /dev/null +++ b/dom/system/gonk/MozMtpCommon.h @@ -0,0 +1,56 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_mozmtpcommon_h__ +#define mozilla_system_mozmtpcommon_h__ + +#include "mozilla/Types.h" +#include <android/log.h> + +#define USE_DEBUG 0 + +#if USE_DEBUG +#define MTP_DBG(msg, ...) \ + __android_log_print(ANDROID_LOG_DEBUG, "MozMtp", \ + "%s: " msg, __FUNCTION__, ##__VA_ARGS__) +#else +#define MTP_DBG(msg, ...) +#endif + +#define MTP_LOG(msg, ...) \ + __android_log_print(ANDROID_LOG_INFO, "MozMtp", \ + "%s: " msg, __FUNCTION__, ##__VA_ARGS__) + +#define MTP_ERR(msg, ...) \ + __android_log_print(ANDROID_LOG_ERROR, "MozMtp", \ + "%s: " msg, __FUNCTION__, ##__VA_ARGS__) + +#define BEGIN_MTP_NAMESPACE \ + namespace mozilla { namespace system { namespace mtp { +#define END_MTP_NAMESPACE \ + } /* namespace mtp */ } /* namespace system */ } /* namespace mozilla */ +#define USING_MTP_NAMESPACE \ + using namespace mozilla::system::mtp; + +namespace android { + class MOZ_EXPORT MtpServer; + class MOZ_EXPORT MtpStorage; + class MOZ_EXPORT MtpStringBuffer; + class MOZ_EXPORT MtpDatabase; + class MOZ_EXPORT MtpDataPacket; + class MOZ_EXPORT MtpProperty; +} + +#include <mtp.h> +#include <MtpDatabase.h> +#include <MtpObjectInfo.h> +#include <MtpProperty.h> +#include <MtpServer.h> +#include <MtpStorage.h> +#include <MtpStringBuffer.h> +#include <MtpTypes.h> + +#endif // mozilla_system_mtpcommon_h__ diff --git a/dom/system/gonk/MozMtpDatabase.cpp b/dom/system/gonk/MozMtpDatabase.cpp new file mode 100644 index 000000000..29fe23e8d --- /dev/null +++ b/dom/system/gonk/MozMtpDatabase.cpp @@ -0,0 +1,1542 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MozMtpDatabase.h" +#include "MozMtpServer.h" + +#include "base/message_loop.h" +#include "DeviceStorage.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Scoped.h" +#include "mozilla/Services.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsPrintfCString.h" +#include "nsString.h" +#include "prio.h" + +#include <dirent.h> +#include <libgen.h> +#include <utime.h> +#include <sys/stat.h> + +using namespace android; +using namespace mozilla; + +namespace mozilla { +MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedCloseDir, PRDir, PR_CloseDir) +} + +BEGIN_MTP_NAMESPACE + +static const char* kMtpWatcherNotify = "mtp-watcher-notify"; + +#if 0 +// Some debug code for figuring out deadlocks, if you happen to run into +// that scenario + +class DebugMutexAutoLock: public MutexAutoLock +{ +public: + DebugMutexAutoLock(mozilla::Mutex& aMutex) + : MutexAutoLock(aMutex) + { + MTP_LOG("Mutex acquired"); + } + + ~DebugMutexAutoLock() + { + MTP_LOG("Releasing mutex"); + } +}; +#define MutexAutoLock MTP_LOG("About to enter mutex"); DebugMutexAutoLock + +#endif + +static const char * +ObjectPropertyAsStr(MtpObjectProperty aProperty) +{ + switch (aProperty) { + case MTP_PROPERTY_STORAGE_ID: return "MTP_PROPERTY_STORAGE_ID"; + case MTP_PROPERTY_OBJECT_FORMAT: return "MTP_PROPERTY_OBJECT_FORMAT"; + case MTP_PROPERTY_PROTECTION_STATUS: return "MTP_PROPERTY_PROTECTION_STATUS"; + case MTP_PROPERTY_OBJECT_SIZE: return "MTP_PROPERTY_OBJECT_SIZE"; + case MTP_PROPERTY_OBJECT_FILE_NAME: return "MTP_PROPERTY_OBJECT_FILE_NAME"; + case MTP_PROPERTY_DATE_CREATED: return "MTP_PROPERTY_DATE_CREATED"; + case MTP_PROPERTY_DATE_MODIFIED: return "MTP_PROPERTY_DATE_MODIFIED"; + case MTP_PROPERTY_PARENT_OBJECT: return "MTP_PROPERTY_PARENT_OBJECT"; + case MTP_PROPERTY_PERSISTENT_UID: return "MTP_PROPERTY_PERSISTENT_UID"; + case MTP_PROPERTY_NAME: return "MTP_PROPERTY_NAME"; + case MTP_PROPERTY_DATE_ADDED: return "MTP_PROPERTY_DATE_ADDED"; + case MTP_PROPERTY_WIDTH: return "MTP_PROPERTY_WIDTH"; + case MTP_PROPERTY_HEIGHT: return "MTP_PROPERTY_HEIGHT"; + case MTP_PROPERTY_IMAGE_BIT_DEPTH: return "MTP_PROPERTY_IMAGE_BIT_DEPTH"; + case MTP_PROPERTY_DISPLAY_NAME: return "MTP_PROPERTY_DISPLAY_NAME"; + } + return "MTP_PROPERTY_???"; +} + +static char* +FormatDate(time_t aTime, char *aDateStr, size_t aDateStrSize) +{ + struct tm tm; + localtime_r(&aTime, &tm); + MTP_LOG("(%ld) tm_zone = %s off = %ld", aTime, tm.tm_zone, tm.tm_gmtoff); + strftime(aDateStr, aDateStrSize, "%Y%m%dT%H%M%S", &tm); + return aDateStr; +} + +MozMtpDatabase::MozMtpDatabase() + : mMutex("MozMtpDatabase::mMutex"), + mDb(mMutex), + mStorage(mMutex), + mBeginSendObjectCalled(false) +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + // We use the index into the array as the handle. Since zero isn't a valid + // index, we stick a dummy entry there. + + RefPtr<DbEntry> dummy; + + MutexAutoLock lock(mMutex); + mDb.AppendElement(dummy); +} + +//virtual +MozMtpDatabase::~MozMtpDatabase() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); +} + +void +MozMtpDatabase::AddEntry(DbEntry *entry) +{ + MutexAutoLock lock(mMutex); + + entry->mHandle = GetNextHandle(); + MOZ_ASSERT(mDb.Length() == entry->mHandle); + mDb.AppendElement(entry); + + MTP_DBG("Handle: 0x%08x Parent: 0x%08x Path:'%s'", + entry->mHandle, entry->mParent, entry->mPath.get()); +} + +void +MozMtpDatabase::AddEntryAndNotify(DbEntry* entry, RefCountedMtpServer* aMtpServer) +{ + AddEntry(entry); + aMtpServer->sendObjectAdded(entry->mHandle); +} + +void +MozMtpDatabase::DumpEntries(const char* aLabel) +{ + MutexAutoLock lock(mMutex); + + ProtectedDbArray::size_type numEntries = mDb.Length(); + MTP_LOG("%s: numEntries = %d", aLabel, numEntries); + ProtectedDbArray::index_type entryIndex; + for (entryIndex = 1; entryIndex < numEntries; entryIndex++) { + RefPtr<DbEntry> entry = mDb[entryIndex]; + if (entry) { + MTP_LOG("%s: mDb[%d]: mHandle: 0x%08x mParent: 0x%08x StorageID: 0x%08x path: '%s'", + aLabel, entryIndex, entry->mHandle, entry->mParent, entry->mStorageID, entry->mPath.get()); + } else { + MTP_LOG("%s: mDb[%2d]: entry is NULL", aLabel, entryIndex); + } + } +} + +MtpObjectHandle +MozMtpDatabase::FindEntryByPath(const nsACString& aPath) +{ + MutexAutoLock lock(mMutex); + + ProtectedDbArray::size_type numEntries = mDb.Length(); + ProtectedDbArray::index_type entryIndex; + for (entryIndex = 1; entryIndex < numEntries; entryIndex++) { + RefPtr<DbEntry> entry = mDb[entryIndex]; + if (entry && entry->mPath.Equals(aPath)) { + return entryIndex; + } + } + return 0; +} + +already_AddRefed<MozMtpDatabase::DbEntry> +MozMtpDatabase::GetEntry(MtpObjectHandle aHandle) +{ + MutexAutoLock lock(mMutex); + + RefPtr<DbEntry> entry; + + if (aHandle > 0 && aHandle < mDb.Length()) { + entry = mDb[aHandle]; + } + return entry.forget(); +} + +void +MozMtpDatabase::RemoveEntry(MtpObjectHandle aHandle) +{ + MutexAutoLock lock(mMutex); + if (!IsValidHandle(aHandle)) { + return; + } + + RefPtr<DbEntry> removedEntry = mDb[aHandle]; + mDb[aHandle] = nullptr; + MTP_DBG("0x%08x removed", aHandle); + // if the entry is not a folder, just return. + if (removedEntry->mObjectFormat != MTP_FORMAT_ASSOCIATION) { + return; + } + + // Find out and remove the children of aHandle. + // Since the index for a directory will always be less than the index of any of its children, + // we can remove the entire subtree in one pass. + ProtectedDbArray::size_type numEntries = mDb.Length(); + ProtectedDbArray::index_type entryIndex; + for (entryIndex = aHandle+1; entryIndex < numEntries; entryIndex++) { + RefPtr<DbEntry> entry = mDb[entryIndex]; + if (entry && IsValidHandle(entry->mParent) && !mDb[entry->mParent]) { + mDb[entryIndex] = nullptr; + MTP_DBG("0x%08x removed", aHandle); + } + } +} + +void +MozMtpDatabase::RemoveEntryAndNotify(MtpObjectHandle aHandle, RefCountedMtpServer* aMtpServer) +{ + RemoveEntry(aHandle); + aMtpServer->sendObjectRemoved(aHandle); +} + +void +MozMtpDatabase::UpdateEntryAndNotify(MtpObjectHandle aHandle, DeviceStorageFile* aFile, RefCountedMtpServer* aMtpServer) +{ + UpdateEntry(aHandle, aFile); + aMtpServer->sendObjectAdded(aHandle); +} + + +void +MozMtpDatabase::UpdateEntry(MtpObjectHandle aHandle, DeviceStorageFile* aFile) +{ + MutexAutoLock lock(mMutex); + + RefPtr<DbEntry> entry = mDb[aHandle]; + + int64_t fileSize = 0; + aFile->mFile->GetFileSize(&fileSize); + entry->mObjectSize = fileSize; + + PRTime dateModifiedMsecs; + // GetLastModifiedTime returns msecs + aFile->mFile->GetLastModifiedTime(&dateModifiedMsecs); + entry->mDateModified = dateModifiedMsecs / PR_MSEC_PER_SEC; + entry->mDateCreated = entry->mDateModified; + entry->mDateAdded = entry->mDateModified; + + #if USE_DEBUG + char dateStr[20]; + MTP_DBG("UpdateEntry (0x%08x file %s) modified (%ld) %s", + entry->mHandle, entry->mPath.get(), + entry->mDateModified, + FormatDate(entry->mDateModified, dateStr, sizeof(dateStr))); + #endif +} + + +class MtpWatcherNotifyRunnable final : public Runnable +{ +public: + MtpWatcherNotifyRunnable(nsACString& aStorageName, + nsACString& aPath, + const char* aEventType) + : mStorageName(aStorageName), + mPath(aPath), + mEventType(aEventType) + {} + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + NS_ConvertUTF8toUTF16 storageName(mStorageName); + NS_ConvertUTF8toUTF16 path(mPath); + + RefPtr<DeviceStorageFile> dsf( + new DeviceStorageFile(NS_LITERAL_STRING(DEVICESTORAGE_SDCARD), + storageName, path)); + NS_ConvertUTF8toUTF16 eventType(mEventType); + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + + MTP_DBG("Sending mtp-watcher-notify %s %s %s", + mEventType.get(), mStorageName.get(), mPath.get()); + + obs->NotifyObservers(dsf, kMtpWatcherNotify, eventType.get()); + return NS_OK; + } + +private: + nsCString mStorageName; + nsCString mPath; + nsCString mEventType; +}; + +// MtpWatcherNotify is used to tell DeviceStorage when a file was changed +// through the MTP server. +void +MozMtpDatabase::MtpWatcherNotify(DbEntry* aEntry, const char* aEventType) +{ + // This function gets called from the MozMtpServer::mServerThread + MOZ_ASSERT(!NS_IsMainThread()); + + MTP_DBG("file: %s %s", aEntry->mPath.get(), aEventType); + + // Tell interested parties that a file was created, deleted, or modified. + + RefPtr<StorageEntry> storageEntry; + { + MutexAutoLock lock(mMutex); + + // FindStorage and the mStorage[] access both need to have the mutex held. + StorageArray::index_type storageIndex = FindStorage(aEntry->mStorageID); + if (storageIndex == StorageArray::NoIndex) { + return; + } + storageEntry = mStorage[storageIndex]; + } + + // DeviceStorage wants the storageName and the path relative to the root + // of the storage area, so we need to strip off the storagePath + + nsAutoCString relPath(Substring(aEntry->mPath, + storageEntry->mStoragePath.Length() + 1)); + + RefPtr<MtpWatcherNotifyRunnable> r = + new MtpWatcherNotifyRunnable(storageEntry->mStorageName, relPath, aEventType); + DebugOnly<nsresult> rv = NS_DispatchToMainThread(r); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +// Called to tell the MTP server about new or deleted files, +void +MozMtpDatabase::MtpWatcherUpdate(RefCountedMtpServer* aMtpServer, + DeviceStorageFile* aFile, + const nsACString& aEventType) +{ + // Runs on the MtpWatcherUpdate->mIOThread (see MozMtpServer.cpp) + MOZ_ASSERT(!NS_IsMainThread()); + + // Figure out which storage the belongs to (if any) + + if (!aFile->mFile) { + // No path - don't bother looking. + return; + } + nsString wideFilePath; + aFile->mFile->GetPath(wideFilePath); + NS_ConvertUTF16toUTF8 filePath(wideFilePath); + + nsCString evtType(aEventType); + MTP_LOG("file %s %s", filePath.get(), evtType.get()); + + MtpObjectHandle entryHandle = FindEntryByPath(filePath); + + if (aEventType.EqualsLiteral("modified")) { + // To update the file information to the newest, we remove the entry for + // the existing file, then re-add the entry for the file. + + if (entryHandle != 0) { + // Update entry for the file and tell MTP. + MTP_LOG("About to update handle 0x%08x file %s", entryHandle, filePath.get()); + UpdateEntryAndNotify(entryHandle, aFile, aMtpServer); + } + else { + // Create entry for the file and tell MTP. + CreateEntryForFileAndNotify(filePath, aFile, aMtpServer); + } + return; + } + + if (aEventType.EqualsLiteral("deleted")) { + if (entryHandle == 0) { + // The entry has already been removed. We can't tell MTP. + return; + } + MTP_LOG("About to call sendObjectRemoved Handle 0x%08x file %s", entryHandle, filePath.get()); + RemoveEntryAndNotify(entryHandle, aMtpServer); + return; + } +} + +nsCString +MozMtpDatabase::BaseName(const nsCString& path) +{ + nsCOMPtr<nsIFile> file; + NS_NewNativeLocalFile(path, false, getter_AddRefs(file)); + if (file) { + nsCString leafName; + file->GetNativeLeafName(leafName); + return leafName; + } + return path; +} + +static nsCString +GetPathWithoutFileName(const nsCString& aFullPath) +{ + nsCString path; + + int32_t offset = aFullPath.RFindChar('/'); + if (offset != kNotFound) { + // The trailing slash will be as part of 'path' + path = StringHead(aFullPath, offset + 1); + } + + MTP_LOG("returning '%s'", path.get()); + + return path; +} + +void +MozMtpDatabase::CreateEntryForFileAndNotify(const nsACString& aPath, + DeviceStorageFile* aFile, + RefCountedMtpServer* aMtpServer) +{ + // Find the StorageID that this path corresponds to. + + nsCString remainder; + MtpStorageID storageID = FindStorageIDFor(aPath, remainder); + if (storageID == 0) { + // The path in question isn't for a storage area we're monitoring. + nsCString path(aPath); + return; + } + + bool exists = false; + aFile->mFile->Exists(&exists); + if (!exists) { + // File doesn't exist, no sense telling MTP about it. + // This could happen if Device Storage created and deleted a file right + // away. Since the notifications wind up being async, the file might + // not exist any more. + return; + } + + // Now walk the remaining directories, finding or creating as required. + + MtpObjectHandle parent = MTP_PARENT_ROOT; + bool doFind = true; + int32_t offset = aPath.Length() - remainder.Length(); + int32_t slash; + + do { + nsDependentCSubstring component; + slash = aPath.FindChar('/', offset); + if (slash == kNotFound) { + component.Rebind(aPath, 0, aPath.Length()); + } else { + component.Rebind(aPath, 0 , slash); + } + if (doFind) { + MtpObjectHandle entryHandle = FindEntryByPath(component); + if (entryHandle != 0) { + // We found an entry. + parent = entryHandle; + offset = slash + 1 ; + continue; + } + } + + // We've got a directory component that doesn't exist. This means that all + // further subdirectories won't exist either, so we can skip searching + // for them. + doFind = false; + + // This directory and the file don't exist, create them + + RefPtr<DbEntry> entry = new DbEntry; + + entry->mStorageID = storageID; + entry->mObjectName = Substring(aPath, offset, slash - offset); + entry->mParent = parent; + entry->mDisplayName = entry->mObjectName; + entry->mPath = component; + + if (slash == kNotFound) { + // No slash - this is the file component + entry->mObjectFormat = MTP_FORMAT_DEFINED; + + int64_t fileSize = 0; + aFile->mFile->GetFileSize(&fileSize); + entry->mObjectSize = fileSize; + + // Note: Even though PRTime records usec, GetLastModifiedTime returns + // msecs. + PRTime dateModifiedMsecs; + aFile->mFile->GetLastModifiedTime(&dateModifiedMsecs); + entry->mDateModified = dateModifiedMsecs / PR_MSEC_PER_SEC; + } else { + // Found a slash, this makes this a directory component + entry->mObjectFormat = MTP_FORMAT_ASSOCIATION; + entry->mObjectSize = 0; + time(&entry->mDateModified); + } + entry->mDateCreated = entry->mDateModified; + entry->mDateAdded = entry->mDateModified; + + AddEntryAndNotify(entry, aMtpServer); + MTP_LOG("About to call sendObjectAdded Handle 0x%08x file %s", entry->mHandle, entry->mPath.get()); + + parent = entry->mHandle; + offset = slash + 1; + } while (slash != kNotFound); + + return; +} + +void +MozMtpDatabase::AddDirectory(MtpStorageID aStorageID, + const char* aPath, + MtpObjectHandle aParent) +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + ScopedCloseDir dir; + + if (!(dir = PR_OpenDir(aPath))) { + MTP_ERR("Unable to open directory '%s'", aPath); + return; + } + + PRDirEntry* dirEntry; + while ((dirEntry = PR_ReadDir(dir, PR_SKIP_BOTH))) { + nsPrintfCString filename("%s/%s", aPath, dirEntry->name); + PRFileInfo64 fileInfo; + if (PR_GetFileInfo64(filename.get(), &fileInfo) != PR_SUCCESS) { + MTP_ERR("Unable to retrieve file information for '%s'", filename.get()); + continue; + } + + RefPtr<DbEntry> entry = new DbEntry; + + entry->mStorageID = aStorageID; + entry->mParent = aParent; + entry->mObjectName = dirEntry->name; + entry->mDisplayName = dirEntry->name; + entry->mPath = filename; + + // PR_GetFileInfo64 returns timestamps in usecs + entry->mDateModified = fileInfo.modifyTime / PR_USEC_PER_SEC; + entry->mDateCreated = fileInfo.creationTime / PR_USEC_PER_SEC; + time(&entry->mDateAdded); + + if (fileInfo.type == PR_FILE_FILE) { + entry->mObjectFormat = MTP_FORMAT_DEFINED; + //TODO: Check how 64-bit filesize are dealt with + entry->mObjectSize = fileInfo.size; + AddEntry(entry); + } else if (fileInfo.type == PR_FILE_DIRECTORY) { + entry->mObjectFormat = MTP_FORMAT_ASSOCIATION; + entry->mObjectSize = 0; + AddEntry(entry); + AddDirectory(aStorageID, filename.get(), entry->mHandle); + } + } +} + +MozMtpDatabase::StorageArray::index_type +MozMtpDatabase::FindStorage(MtpStorageID aStorageID) +{ + // Currently, this routine is called from MozMtpDatabase::RemoveStorage + // and MozMtpDatabase::MtpWatcherNotify, which both hold mMutex. + + StorageArray::size_type numStorages = mStorage.Length(); + StorageArray::index_type storageIndex; + + for (storageIndex = 0; storageIndex < numStorages; storageIndex++) { + RefPtr<StorageEntry> storage = mStorage[storageIndex]; + if (storage->mStorageID == aStorageID) { + return storageIndex; + } + } + return StorageArray::NoIndex; +} + +// Find the storage ID for the storage area that contains aPath. +MtpStorageID +MozMtpDatabase::FindStorageIDFor(const nsACString& aPath, nsCSubstring& aRemainder) +{ + MutexAutoLock lock(mMutex); + + aRemainder.Truncate(); + + StorageArray::size_type numStorages = mStorage.Length(); + StorageArray::index_type storageIndex; + + for (storageIndex = 0; storageIndex < numStorages; storageIndex++) { + RefPtr<StorageEntry> storage = mStorage[storageIndex]; + if (StringHead(aPath, storage->mStoragePath.Length()).Equals(storage->mStoragePath)) { + if (aPath.Length() == storage->mStoragePath.Length()) { + return storage->mStorageID; + } + if (aPath[storage->mStoragePath.Length()] == '/') { + aRemainder = Substring(aPath, storage->mStoragePath.Length() + 1); + return storage->mStorageID; + } + } + } + return 0; +} + +void +MozMtpDatabase::AddStorage(MtpStorageID aStorageID, + const char* aPath, + const char* aName) +{ + // This is called on the IOThread from MozMtpStorage::StorageAvailable + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + MTP_DBG("StorageID: 0x%08x aPath: '%s' aName: '%s'", + aStorageID, aPath, aName); + + PRFileInfo fileInfo; + if (PR_GetFileInfo(aPath, &fileInfo) != PR_SUCCESS) { + MTP_ERR("'%s' doesn't exist", aPath); + return; + } + if (fileInfo.type != PR_FILE_DIRECTORY) { + MTP_ERR("'%s' isn't a directory", aPath); + return; + } + + RefPtr<StorageEntry> storageEntry = new StorageEntry; + + storageEntry->mStorageID = aStorageID; + storageEntry->mStoragePath = aPath; + storageEntry->mStorageName = aName; + { + MutexAutoLock lock(mMutex); + mStorage.AppendElement(storageEntry); + } + + AddDirectory(aStorageID, aPath, MTP_PARENT_ROOT); + { + MutexAutoLock lock(mMutex); + MTP_LOG("added %d items from tree '%s'", mDb.Length(), aPath); + } +} + +void +MozMtpDatabase::RemoveStorage(MtpStorageID aStorageID) +{ + MutexAutoLock lock(mMutex); + + // This is called on the IOThread from MozMtpStorage::StorageAvailable + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + ProtectedDbArray::size_type numEntries = mDb.Length(); + ProtectedDbArray::index_type entryIndex; + for (entryIndex = 1; entryIndex < numEntries; entryIndex++) { + RefPtr<DbEntry> entry = mDb[entryIndex]; + if (entry && entry->mStorageID == aStorageID) { + mDb[entryIndex] = nullptr; + } + } + StorageArray::index_type storageIndex = FindStorage(aStorageID); + if (storageIndex != StorageArray::NoIndex) { + mStorage.RemoveElementAt(storageIndex); + } +} + +// called from SendObjectInfo to reserve a database entry for the incoming file +//virtual +MtpObjectHandle +MozMtpDatabase::beginSendObject(const char* aPath, + MtpObjectFormat aFormat, + MtpObjectHandle aParent, + MtpStorageID aStorageID, + uint64_t aSize, + time_t aModified) +{ + // If MtpServer::doSendObjectInfo receives a request with a parent of + // MTP_PARENT_ROOT, then it fills in aPath with the fully qualified path + // and then passes in a parent of zero. + + if (aParent == 0) { + // Undo what doSendObjectInfo did + aParent = MTP_PARENT_ROOT; + } + + RefPtr<DbEntry> entry = new DbEntry; + + entry->mStorageID = aStorageID; + entry->mParent = aParent; + entry->mPath = aPath; + entry->mObjectName = BaseName(entry->mPath); + entry->mDisplayName = entry->mObjectName; + entry->mObjectFormat = aFormat; + entry->mObjectSize = aSize; + + if (aModified != 0) { + // Currently, due to the way that parseDateTime is coded in + // frameworks/av/media/mtp/MtpUtils.cpp, aModified winds up being the number + // of seconds from the epoch in local time, rather than UTC time. So we + // need to convert it back to being relative to UTC since that's what linux + // expects time_t to contain. + // + // In more concrete testable terms, if the host parses 2015-08-02 02:22:00 + // as a local time in the Pacific timezone, aModified will come to us as + // 1438482120. + // + // What we want is what mktime would pass us with the same date. Using python + // (because its simple) with the current timezone set to be America/Vancouver: + // + // >>> import time + // >>> time.mktime((2015, 8, 2, 2, 22, 0, 0, 0, -1)) + // 1438507320.0 + // >>> time.localtime(1438507320) + // time.struct_time(tm_year=2015, tm_mon=8, tm_mday=2, tm_hour=2, tm_min=22, tm_sec=0, tm_wday=6, tm_yday=214, tm_isdst=1) + // + // Currently, when a file has a modification time of 2015-08-22 02:22:00 PDT + // then aModified will come in as 1438482120 which corresponds to + // 2015-08-22 02:22:00 UTC + + struct tm tm; + if (gmtime_r(&aModified, &tm) != NULL) { + // GMT always comes back with tm_isdst = 0, so we set it to -1 in order + // to have mktime figure out dst based on the date. + tm.tm_isdst = -1; + aModified = mktime(&tm); + if (aModified == (time_t)-1) { + aModified = 0; + } + } else { + aModified = 0; + } + } + if (aModified == 0) { + // The ubuntu host doesn't pass in the modified/created times in the + // SENDOBJECT packet, so aModified winds up being zero. About the best + // we can do with that is to use the current time. + time(&aModified); + } + + // And just an FYI for anybody else looking at timestamps. Under OSX you + // need to use the Android File Transfer program to copy files into the + // phone. That utility passes in both date modified and date created + // timestamps, but they're both equal to the time that the file was copied + // and not the times that are associated with the files. + + // Now we have aModified in a traditional time_t format, which is the number + // of seconds from the UTC epoch. + + entry->mDateModified = aModified; + entry->mDateCreated = entry->mDateModified; + entry->mDateAdded = entry->mDateModified; + + AddEntry(entry); + + #if USE_DEBUG + char dateStr[20]; + MTP_LOG("Handle: 0x%08x Parent: 0x%08x Path: '%s' aModified %ld %s", + entry->mHandle, aParent, aPath, aModified, + FormatDate(entry->mDateModified, dateStr, sizeof(dateStr))); + #endif + + mBeginSendObjectCalled = true; + return entry->mHandle; +} + +// called to report success or failure of the SendObject file transfer +// success should signal a notification of the new object's creation, +// failure should remove the database entry created in beginSendObject + +//virtual +void +MozMtpDatabase::endSendObject(const char* aPath, + MtpObjectHandle aHandle, + MtpObjectFormat aFormat, + bool aSucceeded) +{ + MTP_LOG("Handle: 0x%08x Path: '%s'", aHandle, aPath); + + if (aSucceeded) { + RefPtr<DbEntry> entry = GetEntry(aHandle); + if (entry) { + // The android MTP server only copies the data in, it doesn't set the + // modified timestamp, so we do that here. + + struct utimbuf new_times; + struct stat sb; + + char dateStr[20]; + MTP_LOG("Path: '%s' setting modified time to (%ld) %s", + entry->mPath.get(), entry->mDateModified, + FormatDate(entry->mDateModified, dateStr, sizeof(dateStr))); + + stat(entry->mPath.get(), &sb); + new_times.actime = sb.st_atime; // Preserve atime + new_times.modtime = entry->mDateModified; + utime(entry->mPath.get(), &new_times); + + MtpWatcherNotify(entry, "modified"); + } + } else { + RemoveEntry(aHandle); + } + mBeginSendObjectCalled = false; +} + +//virtual +MtpObjectHandleList* +MozMtpDatabase::getObjectList(MtpStorageID aStorageID, + MtpObjectFormat aFormat, + MtpObjectHandle aParent) +{ + MTP_LOG("StorageID: 0x%08x Format: 0x%04x Parent: 0x%08x", + aStorageID, aFormat, aParent); + + // aStorageID == 0xFFFFFFFF for all storage + // aFormat == 0 for all formats + // aParent == 0xFFFFFFFF for objects with no parents + // aParent == 0 for all objects + + //TODO: Optimize + + UniquePtr<MtpObjectHandleList> list(new MtpObjectHandleList()); + + MutexAutoLock lock(mMutex); + + ProtectedDbArray::size_type numEntries = mDb.Length(); + ProtectedDbArray::index_type entryIndex; + for (entryIndex = 1; entryIndex < numEntries; entryIndex++) { + RefPtr<DbEntry> entry = mDb[entryIndex]; + if (entry && + (aStorageID == 0xFFFFFFFF || entry->mStorageID == aStorageID) && + (aFormat == 0 || entry->mObjectFormat == aFormat) && + (aParent == 0 || entry->mParent == aParent)) { + list->push(entry->mHandle); + } + } + MTP_LOG(" returning %d items", list->size()); + return list.release(); +} + +//virtual +int +MozMtpDatabase::getNumObjects(MtpStorageID aStorageID, + MtpObjectFormat aFormat, + MtpObjectHandle aParent) +{ + MTP_LOG(""); + + // aStorageID == 0xFFFFFFFF for all storage + // aFormat == 0 for all formats + // aParent == 0xFFFFFFFF for objects with no parents + // aParent == 0 for all objects + + int count = 0; + + MutexAutoLock lock(mMutex); + + ProtectedDbArray::size_type numEntries = mDb.Length(); + ProtectedDbArray::index_type entryIndex; + for (entryIndex = 1; entryIndex < numEntries; entryIndex++) { + RefPtr<DbEntry> entry = mDb[entryIndex]; + if (entry && + (aStorageID == 0xFFFFFFFF || entry->mStorageID == aStorageID) && + (aFormat == 0 || entry->mObjectFormat == aFormat) && + (aParent == 0 || entry->mParent == aParent)) { + count++; + } + } + + MTP_LOG(" returning %d items", count); + return count; +} + +//virtual +MtpObjectFormatList* +MozMtpDatabase::getSupportedPlaybackFormats() +{ + static const uint16_t init_data[] = {MTP_FORMAT_UNDEFINED, MTP_FORMAT_ASSOCIATION, + MTP_FORMAT_TEXT, MTP_FORMAT_HTML, MTP_FORMAT_WAV, + MTP_FORMAT_MP3, MTP_FORMAT_MPEG, MTP_FORMAT_EXIF_JPEG, + MTP_FORMAT_TIFF_EP, MTP_FORMAT_BMP, MTP_FORMAT_GIF, + MTP_FORMAT_PNG, MTP_FORMAT_TIFF, MTP_FORMAT_WMA, + MTP_FORMAT_OGG, MTP_FORMAT_AAC, MTP_FORMAT_MP4_CONTAINER, + MTP_FORMAT_MP2, MTP_FORMAT_3GP_CONTAINER, MTP_FORMAT_FLAC}; + + MtpObjectFormatList *list = new MtpObjectFormatList(); + list->appendArray(init_data, MOZ_ARRAY_LENGTH(init_data)); + + MTP_LOG("returning Supported Playback Formats"); + return list; +} + +//virtual +MtpObjectFormatList* +MozMtpDatabase::getSupportedCaptureFormats() +{ + static const uint16_t init_data[] = {MTP_FORMAT_ASSOCIATION, MTP_FORMAT_PNG}; + + MtpObjectFormatList *list = new MtpObjectFormatList(); + list->appendArray(init_data, MOZ_ARRAY_LENGTH(init_data)); + MTP_LOG("returning MTP_FORMAT_ASSOCIATION, MTP_FORMAT_PNG"); + return list; +} + +static const MtpObjectProperty sSupportedObjectProperties[] = +{ + MTP_PROPERTY_STORAGE_ID, + MTP_PROPERTY_OBJECT_FORMAT, + MTP_PROPERTY_PROTECTION_STATUS, // UINT16 - always 0 + MTP_PROPERTY_OBJECT_SIZE, + MTP_PROPERTY_OBJECT_FILE_NAME, // just the filename - no directory + MTP_PROPERTY_NAME, + MTP_PROPERTY_DATE_CREATED, + MTP_PROPERTY_DATE_MODIFIED, + MTP_PROPERTY_PARENT_OBJECT, + MTP_PROPERTY_PERSISTENT_UID, + MTP_PROPERTY_DATE_ADDED, +}; + +//virtual +MtpObjectPropertyList* +MozMtpDatabase::getSupportedObjectProperties(MtpObjectFormat aFormat) +{ + MTP_LOG(""); + MtpObjectPropertyList *list = new MtpObjectPropertyList(); + list->appendArray(sSupportedObjectProperties, + MOZ_ARRAY_LENGTH(sSupportedObjectProperties)); + return list; +} + +//virtual +MtpDevicePropertyList* +MozMtpDatabase::getSupportedDeviceProperties() +{ + MTP_LOG(""); + static const uint16_t init_data[] = { MTP_DEVICE_PROPERTY_UNDEFINED }; + + MtpDevicePropertyList *list = new MtpDevicePropertyList(); + list->appendArray(init_data, MOZ_ARRAY_LENGTH(init_data)); + return list; +} + +//virtual +MtpResponseCode +MozMtpDatabase::getObjectPropertyValue(MtpObjectHandle aHandle, + MtpObjectProperty aProperty, + MtpDataPacket& aPacket) +{ + RefPtr<DbEntry> entry = GetEntry(aHandle); + if (!entry) { + MTP_ERR("Invalid Handle: 0x%08x", aHandle); + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + + MTP_LOG("Handle: 0x%08x '%s' Property: %s 0x%08x", + aHandle, entry->mDisplayName.get(), ObjectPropertyAsStr(aProperty), aProperty); + + switch (aProperty) + { + case MTP_PROPERTY_STORAGE_ID: aPacket.putUInt32(entry->mStorageID); break; + case MTP_PROPERTY_PARENT_OBJECT: aPacket.putUInt32(entry->mParent); break; + case MTP_PROPERTY_OBJECT_FORMAT: aPacket.putUInt16(entry->mObjectFormat); break; + case MTP_PROPERTY_OBJECT_SIZE: aPacket.putUInt64(entry->mObjectSize); break; + case MTP_PROPERTY_DISPLAY_NAME: aPacket.putString(entry->mDisplayName.get()); break; + case MTP_PROPERTY_PERSISTENT_UID: + // the same as aPacket.putUInt128 + aPacket.putUInt64(entry->mHandle); + aPacket.putUInt64(entry->mStorageID); + break; + case MTP_PROPERTY_NAME: aPacket.putString(entry->mDisplayName.get()); break; + + default: + MTP_LOG("Invalid Property: 0x%08x", aProperty); + return MTP_RESPONSE_INVALID_OBJECT_PROP_CODE; + } + + return MTP_RESPONSE_OK; +} + +static int +GetTypeOfObjectProp(MtpObjectProperty aProperty) +{ + struct PropertyTableEntry { + MtpObjectProperty property; + int type; + }; + + static const PropertyTableEntry kObjectPropertyTable[] = { + {MTP_PROPERTY_STORAGE_ID, MTP_TYPE_UINT32 }, + {MTP_PROPERTY_OBJECT_FORMAT, MTP_TYPE_UINT16 }, + {MTP_PROPERTY_PROTECTION_STATUS, MTP_TYPE_UINT16 }, + {MTP_PROPERTY_OBJECT_SIZE, MTP_TYPE_UINT64 }, + {MTP_PROPERTY_OBJECT_FILE_NAME, MTP_TYPE_STR }, + {MTP_PROPERTY_DATE_CREATED, MTP_TYPE_STR }, + {MTP_PROPERTY_DATE_MODIFIED, MTP_TYPE_STR }, + {MTP_PROPERTY_PARENT_OBJECT, MTP_TYPE_UINT32 }, + {MTP_PROPERTY_DISPLAY_NAME, MTP_TYPE_STR }, + {MTP_PROPERTY_NAME, MTP_TYPE_STR }, + {MTP_PROPERTY_PERSISTENT_UID, MTP_TYPE_UINT128 }, + {MTP_PROPERTY_DATE_ADDED, MTP_TYPE_STR }, + }; + + int count = sizeof(kObjectPropertyTable) / sizeof(kObjectPropertyTable[0]); + const PropertyTableEntry* entryProp = kObjectPropertyTable; + int type = 0; + + for (int i = 0; i < count; ++i, ++entryProp) { + if (entryProp->property == aProperty) { + type = entryProp->type; + break; + } + } + + return type; +} + +//virtual +MtpResponseCode +MozMtpDatabase::setObjectPropertyValue(MtpObjectHandle aHandle, + MtpObjectProperty aProperty, + MtpDataPacket& aPacket) +{ + MTP_LOG("Handle: 0x%08x Property: 0x%08x", aHandle, aProperty); + + // Only support file name change + if (aProperty != MTP_PROPERTY_OBJECT_FILE_NAME) { + MTP_ERR("property 0x%x not supported", aProperty); + return MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED; + } + + if (GetTypeOfObjectProp(aProperty) != MTP_TYPE_STR) { + MTP_ERR("property type 0x%x not supported", GetTypeOfObjectProp(aProperty)); + return MTP_RESPONSE_GENERAL_ERROR; + } + + RefPtr<DbEntry> entry = GetEntry(aHandle); + if (!entry) { + MTP_ERR("Invalid Handle: 0x%08x", aHandle); + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + + MtpStringBuffer buf; + aPacket.getString(buf); + + nsDependentCString newFileName(buf); + nsCString newFileFullPath(GetPathWithoutFileName(entry->mPath) + newFileName); + + if (PR_Rename(entry->mPath.get(), newFileFullPath.get()) != PR_SUCCESS) { + MTP_ERR("Failed to rename '%s' to '%s'", + entry->mPath.get(), newFileFullPath.get()); + return MTP_RESPONSE_GENERAL_ERROR; + } + + MTP_LOG("renamed '%s' to '%s'", entry->mPath.get(), newFileFullPath.get()); + + entry->mPath = newFileFullPath; + entry->mObjectName = BaseName(entry->mPath); + entry->mDisplayName = entry->mObjectName; + + return MTP_RESPONSE_OK; +} + +//virtual +MtpResponseCode +MozMtpDatabase::getDevicePropertyValue(MtpDeviceProperty aProperty, + MtpDataPacket& aPacket) +{ + MTP_LOG("(GENERAL ERROR)"); + return MTP_RESPONSE_GENERAL_ERROR; +} + +//virtual +MtpResponseCode +MozMtpDatabase::setDevicePropertyValue(MtpDeviceProperty aProperty, + MtpDataPacket& aPacket) +{ + MTP_LOG("(NOT SUPPORTED)"); + return MTP_RESPONSE_OPERATION_NOT_SUPPORTED; +} + +//virtual +MtpResponseCode +MozMtpDatabase::resetDeviceProperty(MtpDeviceProperty aProperty) +{ + MTP_LOG("(NOT SUPPORTED)"); + return MTP_RESPONSE_OPERATION_NOT_SUPPORTED; +} + +void +MozMtpDatabase::QueryEntries(MozMtpDatabase::MatchType aMatchType, + uint32_t aMatchField1, + uint32_t aMatchField2, + UnprotectedDbArray &result) +{ + MutexAutoLock lock(mMutex); + + ProtectedDbArray::size_type numEntries = mDb.Length(); + ProtectedDbArray::index_type entryIdx; + RefPtr<DbEntry> entry; + + result.Clear(); + + switch (aMatchType) { + + case MatchAll: + for (entryIdx = 0; entryIdx < numEntries; entryIdx++) { + if (mDb[entryIdx]) { + result.AppendElement(mDb[entryIdx]); + } + } + break; + + case MatchHandle: + for (entryIdx = 0; entryIdx < numEntries; entryIdx++) { + entry = mDb[entryIdx]; + if (entry && entry->mHandle == aMatchField1) { + result.AppendElement(entry); + // Handles are unique - return the one that we found. + return; + } + } + break; + + case MatchParent: + for (entryIdx = 0; entryIdx < numEntries; entryIdx++) { + entry = mDb[entryIdx]; + if (entry && entry->mParent == aMatchField1) { + result.AppendElement(entry); + } + } + break; + + case MatchFormat: + for (entryIdx = 0; entryIdx < numEntries; entryIdx++) { + entry = mDb[entryIdx]; + if (entry && entry->mObjectFormat == aMatchField1) { + result.AppendElement(entry); + } + } + break; + + case MatchHandleFormat: + for (entryIdx = 0; entryIdx < numEntries; entryIdx++) { + entry = mDb[entryIdx]; + if (entry && entry->mHandle == aMatchField1) { + if (entry->mObjectFormat == aMatchField2) { + result.AppendElement(entry); + } + // Only 1 entry can match my aHandle. So we can return early. + return; + } + } + break; + + case MatchParentFormat: + for (entryIdx = 0; entryIdx < numEntries; entryIdx++) { + entry = mDb[entryIdx]; + if (entry && entry->mParent == aMatchField1 && entry->mObjectFormat == aMatchField2) { + result.AppendElement(entry); + } + } + break; + + default: + MOZ_ASSERT(!"Invalid MatchType"); + } +} + +//virtual +MtpResponseCode +MozMtpDatabase::getObjectPropertyList(MtpObjectHandle aHandle, + uint32_t aFormat, + uint32_t aProperty, + int aGroupCode, + int aDepth, + MtpDataPacket& aPacket) +{ + MTP_LOG("Handle: 0x%08x Format: 0x%08x aProperty: 0x%08x aGroupCode: %d aDepth %d", + aHandle, aFormat, aProperty, aGroupCode, aDepth); + + if (aDepth > 1) { + return MTP_RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED; + } + if (aGroupCode != 0) { + return MTP_RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED; + } + + MatchType matchType = MatchAll; + uint32_t matchField1 = 0; + uint32_t matchField2 = 0; + + // aHandle == 0 implies all objects at the root level + // further specificed by aFormat and/or aDepth + + if (aFormat == 0) { + if (aHandle == 0xffffffff) { + // select all objects + matchType = MatchAll; + } else { + if (aDepth == 1) { + // select objects whose Parent matches aHandle + matchType = MatchParent; + matchField1 = aHandle; + } else { + // select object whose handle matches aHandle + matchType = MatchHandle; + matchField1 = aHandle; + } + } + } else { + if (aHandle == 0xffffffff) { + // select all objects whose format matches aFormat + matchType = MatchFormat; + matchField1 = aFormat; + } else { + if (aDepth == 1) { + // select objects whose Parent is aHandle and format matches aFormat + matchType = MatchParentFormat; + matchField1 = aHandle; + matchField2 = aFormat; + } else { + // select objects whose handle is aHandle and format matches aFormat + matchType = MatchHandleFormat; + matchField1 = aHandle; + matchField2 = aFormat; + } + } + } + + UnprotectedDbArray result; + QueryEntries(matchType, matchField1, matchField2, result); + + const MtpObjectProperty *objectPropertyList; + size_t numObjectProperties = 0; + MtpObjectProperty objectProperty; + + if (aProperty == 0xffffffff) { + // return all supported properties + numObjectProperties = MOZ_ARRAY_LENGTH(sSupportedObjectProperties); + objectPropertyList = sSupportedObjectProperties; + } else { + // return property indicated by aProperty + numObjectProperties = 1; + objectProperty = aProperty; + objectPropertyList = &objectProperty; + } + + UnprotectedDbArray::size_type numEntries = result.Length(); + UnprotectedDbArray::index_type entryIdx; + + char dateStr[20]; + + aPacket.putUInt32(numObjectProperties * numEntries); + for (entryIdx = 0; entryIdx < numEntries; entryIdx++) { + RefPtr<DbEntry> entry = result[entryIdx]; + + for (size_t propertyIdx = 0; propertyIdx < numObjectProperties; propertyIdx++) { + aPacket.putUInt32(entry->mHandle); + MtpObjectProperty prop = objectPropertyList[propertyIdx]; + aPacket.putUInt16(prop); + switch (prop) { + + case MTP_PROPERTY_STORAGE_ID: + aPacket.putUInt16(MTP_TYPE_UINT32); + aPacket.putUInt32(entry->mStorageID); + break; + + case MTP_PROPERTY_PARENT_OBJECT: + aPacket.putUInt16(MTP_TYPE_UINT32); + aPacket.putUInt32(entry->mParent); + break; + + case MTP_PROPERTY_PERSISTENT_UID: + aPacket.putUInt16(MTP_TYPE_UINT128); + // the same as aPacket.putUInt128 + aPacket.putUInt64(entry->mHandle); + aPacket.putUInt64(entry->mStorageID); + break; + + case MTP_PROPERTY_OBJECT_FORMAT: + aPacket.putUInt16(MTP_TYPE_UINT16); + aPacket.putUInt16(entry->mObjectFormat); + break; + + case MTP_PROPERTY_OBJECT_SIZE: + aPacket.putUInt16(MTP_TYPE_UINT64); + aPacket.putUInt64(entry->mObjectSize); + break; + + case MTP_PROPERTY_OBJECT_FILE_NAME: + case MTP_PROPERTY_NAME: + aPacket.putUInt16(MTP_TYPE_STR); + aPacket.putString(entry->mObjectName.get()); + break; + + case MTP_PROPERTY_PROTECTION_STATUS: + aPacket.putUInt16(MTP_TYPE_UINT16); + aPacket.putUInt16(0); // 0 = No Protection + break; + + case MTP_PROPERTY_DATE_CREATED: { + aPacket.putUInt16(MTP_TYPE_STR); + aPacket.putString(FormatDate(entry->mDateCreated, dateStr, sizeof(dateStr))); + MTP_LOG("mDateCreated: (%ld) %s", entry->mDateCreated, dateStr); + break; + } + + case MTP_PROPERTY_DATE_MODIFIED: { + aPacket.putUInt16(MTP_TYPE_STR); + aPacket.putString(FormatDate(entry->mDateModified, dateStr, sizeof(dateStr))); + MTP_LOG("mDateModified: (%ld) %s", entry->mDateModified, dateStr); + break; + } + + case MTP_PROPERTY_DATE_ADDED: { + aPacket.putUInt16(MTP_TYPE_STR); + aPacket.putString(FormatDate(entry->mDateAdded, dateStr, sizeof(dateStr))); + MTP_LOG("mDateAdded: (%ld) %s", entry->mDateAdded, dateStr); + break; + } + + default: + MTP_ERR("Unrecognized property code: %u", prop); + return MTP_RESPONSE_GENERAL_ERROR; + } + } + } + return MTP_RESPONSE_OK; +} + +//virtual +MtpResponseCode +MozMtpDatabase::getObjectInfo(MtpObjectHandle aHandle, + MtpObjectInfo& aInfo) +{ + RefPtr<DbEntry> entry = GetEntry(aHandle); + if (!entry) { + MTP_ERR("Handle 0x%08x is invalid", aHandle); + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + + MTP_LOG("Handle: 0x%08x Display:'%s' Object:'%s'", aHandle, entry->mDisplayName.get(), entry->mObjectName.get()); + + aInfo.mHandle = aHandle; + aInfo.mStorageID = entry->mStorageID; + aInfo.mFormat = entry->mObjectFormat; + aInfo.mProtectionStatus = 0x0; + + if (entry->mObjectSize > 0xFFFFFFFFuLL) { + aInfo.mCompressedSize = 0xFFFFFFFFuLL; + } else { + aInfo.mCompressedSize = entry->mObjectSize; + } + + aInfo.mThumbFormat = MTP_FORMAT_UNDEFINED; + aInfo.mThumbCompressedSize = 0; + aInfo.mThumbPixWidth = 0; + aInfo.mThumbPixHeight = 0; + aInfo.mImagePixWidth = 0; + aInfo.mImagePixHeight = 0; + aInfo.mImagePixDepth = 0; + aInfo.mParent = entry->mParent; + aInfo.mAssociationType = 0; + aInfo.mAssociationDesc = 0; + aInfo.mSequenceNumber = 0; + aInfo.mName = ::strdup(entry->mObjectName.get()); + aInfo.mDateCreated = entry->mDateCreated; + aInfo.mDateModified = entry->mDateModified; + + MTP_LOG("aInfo.mDateCreated = %ld entry->mDateCreated = %ld", + aInfo.mDateCreated, entry->mDateCreated); + MTP_LOG("aInfo.mDateModified = %ld entry->mDateModified = %ld", + aInfo.mDateModified, entry->mDateModified); + + aInfo.mKeywords = ::strdup("fxos,touch"); + + return MTP_RESPONSE_OK; +} + +//virtual +void* +MozMtpDatabase::getThumbnail(MtpObjectHandle aHandle, size_t& aOutThumbSize) +{ + MTP_LOG("Handle: 0x%08x (returning nullptr)", aHandle); + + aOutThumbSize = 0; + + return nullptr; +} + +//virtual +MtpResponseCode +MozMtpDatabase::getObjectFilePath(MtpObjectHandle aHandle, + MtpString& aOutFilePath, + int64_t& aOutFileLength, + MtpObjectFormat& aOutFormat) +{ + RefPtr<DbEntry> entry = GetEntry(aHandle); + if (!entry) { + MTP_ERR("Handle 0x%08x is invalid", aHandle); + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + + MTP_LOG("Handle: 0x%08x FilePath: '%s'", aHandle, entry->mPath.get()); + + aOutFilePath = entry->mPath.get(); + aOutFileLength = entry->mObjectSize; + aOutFormat = entry->mObjectFormat; + + return MTP_RESPONSE_OK; +} + +//virtual +MtpResponseCode +MozMtpDatabase::deleteFile(MtpObjectHandle aHandle) +{ + RefPtr<DbEntry> entry = GetEntry(aHandle); + if (!entry) { + MTP_ERR("Invalid Handle: 0x%08x", aHandle); + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } + + MTP_LOG("Handle: 0x%08x '%s'", aHandle, entry->mPath.get()); + + // File deletion will happen in lower level implementation. + // The only thing we need to do is removing the entry from the db. + RemoveEntry(aHandle); + + // Tell Device Storage that the file is gone. + MtpWatcherNotify(entry, "deleted"); + + return MTP_RESPONSE_OK; +} + +#if 0 +//virtual +MtpResponseCode +MozMtpDatabase::moveFile(MtpObjectHandle aHandle, MtpObjectHandle aNewParent) +{ + MTP_LOG("Handle: 0x%08x NewParent: 0x%08x", aHandle, aNewParent); + + // change parent + + return MTP_RESPONSE_OK +} + +//virtual +MtpResponseCode +MozMtpDatabase::copyFile(MtpObjectHandle aHandle, MtpObjectHandle aNewParent) +{ + MTP_LOG("Handle: 0x%08x NewParent: 0x%08x", aHandle, aNewParent); + + // duplicate DbEntry + // change parent + + return MTP_RESPONSE_OK +} +#endif + +//virtual +MtpObjectHandleList* +MozMtpDatabase::getObjectReferences(MtpObjectHandle aHandle) +{ + MTP_LOG("Handle: 0x%08x (returning nullptr)", aHandle); + return nullptr; +} + +//virtual +MtpResponseCode +MozMtpDatabase::setObjectReferences(MtpObjectHandle aHandle, + MtpObjectHandleList* aReferences) +{ + MTP_LOG("Handle: 0x%08x (NOT SUPPORTED)", aHandle); + return MTP_RESPONSE_OPERATION_NOT_SUPPORTED; +} + +//virtual +MtpProperty* +MozMtpDatabase::getObjectPropertyDesc(MtpObjectProperty aProperty, + MtpObjectFormat aFormat) +{ + MTP_LOG("Property: %s 0x%08x", ObjectPropertyAsStr(aProperty), aProperty); + + MtpProperty* result = nullptr; + switch (aProperty) + { + case MTP_PROPERTY_PROTECTION_STATUS: + result = new MtpProperty(aProperty, MTP_TYPE_UINT16); + break; + case MTP_PROPERTY_OBJECT_FORMAT: + result = new MtpProperty(aProperty, MTP_TYPE_UINT16, false, aFormat); + break; + case MTP_PROPERTY_STORAGE_ID: + case MTP_PROPERTY_PARENT_OBJECT: + case MTP_PROPERTY_WIDTH: + case MTP_PROPERTY_HEIGHT: + case MTP_PROPERTY_IMAGE_BIT_DEPTH: + result = new MtpProperty(aProperty, MTP_TYPE_UINT32); + break; + case MTP_PROPERTY_OBJECT_SIZE: + result = new MtpProperty(aProperty, MTP_TYPE_UINT64); + break; + case MTP_PROPERTY_DISPLAY_NAME: + case MTP_PROPERTY_NAME: + result = new MtpProperty(aProperty, MTP_TYPE_STR); + break; + case MTP_PROPERTY_OBJECT_FILE_NAME: + result = new MtpProperty(aProperty, MTP_TYPE_STR, true); + break; + case MTP_PROPERTY_DATE_CREATED: + case MTP_PROPERTY_DATE_MODIFIED: + case MTP_PROPERTY_DATE_ADDED: + result = new MtpProperty(aProperty, MTP_TYPE_STR); + result->setFormDateTime(); + break; + case MTP_PROPERTY_PERSISTENT_UID: + result = new MtpProperty(aProperty, MTP_TYPE_UINT128); + break; + default: + break; + } + + return result; +} + +//virtual +MtpProperty* +MozMtpDatabase::getDevicePropertyDesc(MtpDeviceProperty aProperty) +{ + MTP_LOG("(returning MTP_DEVICE_PROPERTY_UNDEFINED)"); + return new MtpProperty(MTP_DEVICE_PROPERTY_UNDEFINED, MTP_TYPE_UNDEFINED); +} + +//virtual +void +MozMtpDatabase::sessionStarted() +{ + MTP_LOG(""); +} + +//virtual +void +MozMtpDatabase::sessionEnded() +{ + MTP_LOG(""); +} + +END_MTP_NAMESPACE diff --git a/dom/system/gonk/MozMtpDatabase.h b/dom/system/gonk/MozMtpDatabase.h new file mode 100644 index 000000000..8b308762e --- /dev/null +++ b/dom/system/gonk/MozMtpDatabase.h @@ -0,0 +1,288 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_mozmtpdatabase_h__ +#define mozilla_system_mozmtpdatabase_h__ + +#include "MozMtpCommon.h" + +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIThread.h" +#include "nsTArray.h" + +class DeviceStorageFile; + +BEGIN_MTP_NAMESPACE // mozilla::system::mtp + +class RefCountedMtpServer; + +using namespace android; + +class MozMtpDatabase final : public MtpDatabase +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MozMtpDatabase) + + MozMtpDatabase(); + + // called from SendObjectInfo to reserve a database entry for the incoming file + virtual MtpObjectHandle beginSendObject(const char* aPath, + MtpObjectFormat aFormat, + MtpObjectHandle aParent, + MtpStorageID aStorageID, + uint64_t aSize, + time_t aModified); + + // called to report success or failure of the SendObject file transfer + // success should signal a notification of the new object's creation, + // failure should remove the database entry created in beginSendObject + virtual void endSendObject(const char* aPath, + MtpObjectHandle aHandle, + MtpObjectFormat aFormat, + bool aSucceeded); + + virtual MtpObjectHandleList* getObjectList(MtpStorageID aStorageID, + MtpObjectFormat aFormat, + MtpObjectHandle aParent); + + virtual int getNumObjects(MtpStorageID aStorageID, + MtpObjectFormat aFormat, + MtpObjectHandle aParent); + + virtual MtpObjectFormatList* getSupportedPlaybackFormats(); + + virtual MtpObjectFormatList* getSupportedCaptureFormats(); + + virtual MtpObjectPropertyList* getSupportedObjectProperties(MtpObjectFormat aFormat); + + virtual MtpDevicePropertyList* getSupportedDeviceProperties(); + + virtual MtpResponseCode getObjectPropertyValue(MtpObjectHandle aHandle, + MtpObjectProperty aProperty, + MtpDataPacket& aPacket); + + virtual MtpResponseCode setObjectPropertyValue(MtpObjectHandle aHandle, + MtpObjectProperty aProperty, + MtpDataPacket& aPacket); + + virtual MtpResponseCode getDevicePropertyValue(MtpDeviceProperty aProperty, + MtpDataPacket& aPacket); + + virtual MtpResponseCode setDevicePropertyValue(MtpDeviceProperty aProperty, + MtpDataPacket& aPacket); + + virtual MtpResponseCode resetDeviceProperty(MtpDeviceProperty aProperty); + + virtual MtpResponseCode getObjectPropertyList(MtpObjectHandle aHandle, + uint32_t aFormat, + uint32_t aProperty, + int aGroupCode, + int aDepth, + MtpDataPacket& aPacket); + + virtual MtpResponseCode getObjectInfo(MtpObjectHandle aHandle, + MtpObjectInfo& aInfo); + + virtual void* getThumbnail(MtpObjectHandle aHandle, size_t& aOutThumbSize); + + virtual MtpResponseCode getObjectFilePath(MtpObjectHandle aHandle, + MtpString& aOutFilePath, + int64_t& aOutFileLength, + MtpObjectFormat& aOutFormat); + + virtual MtpResponseCode deleteFile(MtpObjectHandle aHandle); + + virtual MtpObjectHandleList* getObjectReferences(MtpObjectHandle aHandle); + + virtual MtpResponseCode setObjectReferences(MtpObjectHandle aHandle, + MtpObjectHandleList* aReferences); + + virtual MtpProperty* getObjectPropertyDesc(MtpObjectProperty aProperty, + MtpObjectFormat aFormat); + + virtual MtpProperty* getDevicePropertyDesc(MtpDeviceProperty aProperty); + + virtual void sessionStarted(); + + virtual void sessionEnded(); + + void AddStorage(MtpStorageID aStorageID, const char* aPath, const char *aName); + void RemoveStorage(MtpStorageID aStorageID); + + void MtpWatcherUpdate(RefCountedMtpServer* aMtpServer, + DeviceStorageFile* aFile, + const nsACString& aEventType); + +protected: + virtual ~MozMtpDatabase(); + +private: + + struct DbEntry final + { + DbEntry() + : mHandle(0), + mStorageID(0), + mObjectFormat(MTP_FORMAT_DEFINED), + mParent(0), + mObjectSize(0), + mDateCreated(0), + mDateModified(0), + mDateAdded(0) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DbEntry) + + MtpObjectHandle mHandle; // uint32_t + MtpStorageID mStorageID; // uint32_t + nsCString mObjectName; + MtpObjectFormat mObjectFormat; // uint16_t + MtpObjectHandle mParent; // uint32_t + uint64_t mObjectSize; + nsCString mDisplayName; + nsCString mPath; + time_t mDateCreated; + time_t mDateModified; + time_t mDateAdded; + + protected: + ~DbEntry() {} + }; + + template<class T> + class ProtectedTArray : private nsTArray<T> + { + public: + typedef T elem_type; + typedef typename nsTArray<T>::size_type size_type; + typedef typename nsTArray<T>::index_type index_type; + typedef nsTArray<T> base_type; + + static const index_type NoIndex = base_type::NoIndex; + + ProtectedTArray(mozilla::Mutex& aMutex) + : mMutex(aMutex) + {} + + size_type Length() const + { + // GRR - This assert prints to stderr and won't show up in logcat. + mMutex.AssertCurrentThreadOwns(); + return base_type::Length(); + } + + template <class Item> + elem_type* AppendElement(const Item& aItem) + { + mMutex.AssertCurrentThreadOwns(); + return base_type::AppendElement(aItem); + } + + void Clear() + { + mMutex.AssertCurrentThreadOwns(); + base_type::Clear(); + } + + void RemoveElementAt(index_type aIndex) + { + mMutex.AssertCurrentThreadOwns(); + base_type::RemoveElementAt(aIndex); + } + + elem_type& operator[](index_type aIndex) + { + mMutex.AssertCurrentThreadOwns(); + return base_type::ElementAt(aIndex); + } + + const elem_type& operator[](index_type aIndex) const + { + mMutex.AssertCurrentThreadOwns(); + return base_type::ElementAt(aIndex); + } + + private: + mozilla::Mutex& mMutex; + }; + typedef nsTArray<RefPtr<DbEntry> > UnprotectedDbArray; + typedef ProtectedTArray<RefPtr<DbEntry> > ProtectedDbArray; + + struct StorageEntry final + { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(StorageEntry) + + MtpStorageID mStorageID; + nsCString mStoragePath; + nsCString mStorageName; + + protected: + ~StorageEntry() {} + }; + typedef ProtectedTArray<RefPtr<StorageEntry> > StorageArray; + + enum MatchType + { + MatchAll, + MatchHandle, + MatchParent, + MatchFormat, + MatchHandleFormat, + MatchParentFormat, + }; + + bool IsValidHandle(MtpObjectHandle aHandle) + { + return aHandle > 0 && aHandle < mDb.Length(); + } + + void AddEntry(DbEntry* aEntry); + void AddEntryAndNotify(DbEntry* aEntr, RefCountedMtpServer* aMtpServer); + void DumpEntries(const char* aLabel); + MtpObjectHandle FindEntryByPath(const nsACString& aPath); + already_AddRefed<DbEntry> GetEntry(MtpObjectHandle aHandle); + void RemoveEntry(MtpObjectHandle aHandle); + void RemoveEntryAndNotify(MtpObjectHandle aHandle, RefCountedMtpServer* aMtpServer); + void UpdateEntry(MtpObjectHandle aHandle, DeviceStorageFile* aFile); + void UpdateEntryAndNotify(MtpObjectHandle aHandle, DeviceStorageFile* aFile, + RefCountedMtpServer* aMtpServer); + void QueryEntries(MatchType aMatchType, uint32_t aMatchField1, + uint32_t aMatchField2, UnprotectedDbArray& aResult); + + nsCString BaseName(const nsCString& aPath); + + + MtpObjectHandle GetNextHandle() + { + return mDb.Length(); + } + + void AddDirectory(MtpStorageID aStorageID, const char *aPath, MtpObjectHandle aParent); + + void CreateEntryForFileAndNotify(const nsACString& aPath, + DeviceStorageFile* aFile, + RefCountedMtpServer* aMtpServer); + + StorageArray::index_type FindStorage(MtpStorageID aStorageID); + MtpStorageID FindStorageIDFor(const nsACString& aPath, nsCSubstring& aRemainder); + void MtpWatcherNotify(DbEntry* aEntry, const char* aEventType); + + // We need a mutex to protext mDb and mStorage. The MTP server runs on a + // dedicated thread, and it updates/accesses mDb. When files are updated + // through DeviceStorage, we need to update/access mDb and mStorage as well + // (from a non-MTP server thread). + mozilla::Mutex mMutex; + ProtectedDbArray mDb; + StorageArray mStorage; + + bool mBeginSendObjectCalled; +}; + +END_MTP_NAMESPACE + +#endif // mozilla_system_mozmtpdatabase_h__ diff --git a/dom/system/gonk/MozMtpServer.cpp b/dom/system/gonk/MozMtpServer.cpp new file mode 100644 index 000000000..c26b6368b --- /dev/null +++ b/dom/system/gonk/MozMtpServer.cpp @@ -0,0 +1,263 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MozMtpServer.h" +#include "MozMtpDatabase.h" + +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include <errno.h> +#include <stdio.h> +#include <unistd.h> + +#include <cutils/properties.h> +#include <private/android_filesystem_config.h> + +#include "base/message_loop.h" +#include "DeviceStorage.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/Scoped.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsISupportsImpl.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" + +#include "Volume.h" + +#define DEFAULT_THREAD_TIMEOUT_MS 30000 + +using namespace android; +using namespace mozilla; +BEGIN_MTP_NAMESPACE + +static const char* kMtpWatcherUpdate = "mtp-watcher-update"; + +class MtpWatcherUpdateRunnable final : public Runnable +{ +public: + MtpWatcherUpdateRunnable(MozMtpDatabase* aMozMtpDatabase, + RefCountedMtpServer* aMtpServer, + DeviceStorageFile* aFile, + const nsACString& aEventType) + : mMozMtpDatabase(aMozMtpDatabase), + mMtpServer(aMtpServer), + mFile(aFile), + mEventType(aEventType) + {} + + NS_IMETHOD Run() override + { + // Runs on the MtpWatcherUpdate->mIOThread + MOZ_ASSERT(!NS_IsMainThread()); + + mMozMtpDatabase->MtpWatcherUpdate(mMtpServer, mFile, mEventType); + return NS_OK; + } + +private: + RefPtr<MozMtpDatabase> mMozMtpDatabase; + RefPtr<RefCountedMtpServer> mMtpServer; + RefPtr<DeviceStorageFile> mFile; + nsCString mEventType; +}; + +// The MtpWatcherUpdate class listens for mtp-watcher-update events +// and tells the MtpServer about changes made in device storage. +class MtpWatcherUpdate final : public nsIObserver +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + MtpWatcherUpdate(MozMtpServer* aMozMtpServer) + : mMozMtpServer(aMozMtpServer) + { + MOZ_ASSERT(NS_IsMainThread()); + + mIOThread = new LazyIdleThread( + DEFAULT_THREAD_TIMEOUT_MS, + NS_LITERAL_CSTRING("MtpWatcherUpdate")); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + obs->AddObserver(this, kMtpWatcherUpdate, false); + } + + NS_IMETHOD + Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) + { + MOZ_ASSERT(NS_IsMainThread()); + + if (strcmp(aTopic, kMtpWatcherUpdate)) { + // We're only interested in mtp-watcher-update events + return NS_OK; + } + + NS_ConvertUTF16toUTF8 eventType(aData); + if (!eventType.EqualsLiteral("modified") && !eventType.EqualsLiteral("deleted")) { + // Bug 1074604: Needn't handle "created" event, once file operation + // finished, it would trigger "modified" event. + return NS_OK; + } + + DeviceStorageFile* file = static_cast<DeviceStorageFile*>(aSubject); + file->Dump(kMtpWatcherUpdate); + MTP_LOG("%s: file %s %s", kMtpWatcherUpdate, + NS_LossyConvertUTF16toASCII(file->mPath).get(), + eventType.get()); + + RefPtr<MozMtpDatabase> mozMtpDatabase = mMozMtpServer->GetMozMtpDatabase(); + RefPtr<RefCountedMtpServer> mtpServer = mMozMtpServer->GetMtpServer(); + + // We're not supposed to perform I/O on the main thread, so punt the + // notification (which will write to /dev/mtp_usb) to an I/O Thread. + + RefPtr<MtpWatcherUpdateRunnable> r = + new MtpWatcherUpdateRunnable(mozMtpDatabase, mtpServer, file, eventType); + mIOThread->Dispatch(r, NS_DISPATCH_NORMAL); + + return NS_OK; + } + +protected: + ~MtpWatcherUpdate() + { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + obs->RemoveObserver(this, kMtpWatcherUpdate); + } + +private: + RefPtr<MozMtpServer> mMozMtpServer; + nsCOMPtr<nsIThread> mIOThread; +}; +NS_IMPL_ISUPPORTS(MtpWatcherUpdate, nsIObserver) +static StaticRefPtr<MtpWatcherUpdate> sMtpWatcherUpdate; + +class AllocMtpWatcherUpdateRunnable final : public Runnable +{ +public: + AllocMtpWatcherUpdateRunnable(MozMtpServer* aMozMtpServer) + : mMozMtpServer(aMozMtpServer) + {} + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + sMtpWatcherUpdate = new MtpWatcherUpdate(mMozMtpServer); + return NS_OK; + } +private: + RefPtr<MozMtpServer> mMozMtpServer; +}; + +class FreeMtpWatcherUpdateRunnable final : public Runnable +{ +public: + FreeMtpWatcherUpdateRunnable(MozMtpServer* aMozMtpServer) + : mMozMtpServer(aMozMtpServer) + {} + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + sMtpWatcherUpdate = nullptr; + return NS_OK; + } +private: + RefPtr<MozMtpServer> mMozMtpServer; +}; + +class MtpServerRunnable : public Runnable +{ +public: + MtpServerRunnable(int aMtpUsbFd, MozMtpServer* aMozMtpServer) + : mMozMtpServer(aMozMtpServer), + mMtpUsbFd(aMtpUsbFd) + { + } + + nsresult Run() + { + RefPtr<RefCountedMtpServer> server = mMozMtpServer->GetMtpServer(); + + DebugOnly<nsresult> rv = + NS_DispatchToMainThread(new AllocMtpWatcherUpdateRunnable(mMozMtpServer)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + MTP_LOG("MozMtpServer started"); + server->run(); + MTP_LOG("MozMtpServer finished"); + + // server->run will have closed the file descriptor. + mMtpUsbFd.forget(); + + rv = NS_DispatchToMainThread(new FreeMtpWatcherUpdateRunnable(mMozMtpServer)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + return NS_OK; + } + +private: + RefPtr<MozMtpServer> mMozMtpServer; + ScopedClose mMtpUsbFd; // We want to hold this open while the server runs +}; + +already_AddRefed<RefCountedMtpServer> +MozMtpServer::GetMtpServer() +{ + RefPtr<RefCountedMtpServer> server = mMtpServer; + return server.forget(); +} + +already_AddRefed<MozMtpDatabase> +MozMtpServer::GetMozMtpDatabase() +{ + RefPtr<MozMtpDatabase> db = mMozMtpDatabase; + return db.forget(); +} + +bool +MozMtpServer::Init() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + const char *mtpUsbFilename = "/dev/mtp_usb"; + mMtpUsbFd = open(mtpUsbFilename, O_RDWR); + if (mMtpUsbFd.get() < 0) { + MTP_ERR("open of '%s' failed((%s))", mtpUsbFilename, strerror(errno)); + return false; + } + MTP_LOG("Opened '%s' fd %d", mtpUsbFilename, mMtpUsbFd.get()); + + mMozMtpDatabase = new MozMtpDatabase(); + mMtpServer = new RefCountedMtpServer(mMtpUsbFd.get(), // fd + mMozMtpDatabase.get(), // MtpDatabase + false, // ptp? + AID_MEDIA_RW, // file group + 0664, // file permissions + 0775); // dir permissions + return true; +} + +void +MozMtpServer::Run() +{ + nsresult rv = NS_NewNamedThread("MtpServer", getter_AddRefs(mServerThread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + MOZ_ASSERT(mServerThread); + mServerThread->Dispatch(new MtpServerRunnable(mMtpUsbFd.forget(), this), NS_DISPATCH_NORMAL); +} + +END_MTP_NAMESPACE diff --git a/dom/system/gonk/MozMtpServer.h b/dom/system/gonk/MozMtpServer.h new file mode 100644 index 000000000..4989c25ef --- /dev/null +++ b/dom/system/gonk/MozMtpServer.h @@ -0,0 +1,61 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_mozmtpserver_h__ +#define mozilla_system_mozmtpserver_h__ + +#include "MozMtpCommon.h" +#include "MozMtpDatabase.h" + +#include "mozilla/FileUtils.h" + +#include "nsCOMPtr.h" +#include "nsIThread.h" + +BEGIN_MTP_NAMESPACE +using namespace android; + +class RefCountedMtpServer : public MtpServer +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefCountedMtpServer) + + RefCountedMtpServer(int aFd, MtpDatabase* aDatabase, bool aPtp, + int aFileGroup, int aFilePerm, int aDirectoryPerm) + : MtpServer(aFd, aDatabase, aPtp, aFileGroup, aFilePerm, aDirectoryPerm) + { + } + +protected: + virtual ~RefCountedMtpServer() {} +}; + +class MozMtpServer +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MozMtpServer) + + bool Init(); + void Run(); + + already_AddRefed<RefCountedMtpServer> GetMtpServer(); + already_AddRefed<MozMtpDatabase> GetMozMtpDatabase(); + +protected: + virtual ~MozMtpServer() {} + +private: + RefPtr<RefCountedMtpServer> mMtpServer; + RefPtr<MozMtpDatabase> mMozMtpDatabase; + nsCOMPtr<nsIThread> mServerThread; + ScopedClose mMtpUsbFd; +}; + +END_MTP_NAMESPACE + +#endif // mozilla_system_mozmtpserver_h__ + + diff --git a/dom/system/gonk/MozMtpStorage.cpp b/dom/system/gonk/MozMtpStorage.cpp new file mode 100644 index 000000000..9c358a132 --- /dev/null +++ b/dom/system/gonk/MozMtpStorage.cpp @@ -0,0 +1,135 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MozMtpStorage.h" +#include "MozMtpDatabase.h" +#include "MozMtpServer.h" + +#include "base/message_loop.h" +#include "nsXULAppAPI.h" + +BEGIN_MTP_NAMESPACE +using namespace android; + +MozMtpStorage::MozMtpStorage(Volume* aVolume, MozMtpServer* aMozMtpServer) + : mMozMtpServer(aMozMtpServer), + mVolume(aVolume) +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + // The MtpStorageID has the physical volume in the top 16 bits, and the + // logical volumein the lower 16 bits. We treat each volume as a separate + // phsyical storage; + mStorageID = mVolume->Id() << 16 | 1; + + MTP_LOG("Storage constructed for Volume %s mStorageID 0x%08x", + aVolume->NameStr(), mStorageID); + + Volume::RegisterVolumeObserver(this, "MozMtpStorage"); + + // Get things in sync + Notify(mVolume); +} + +MozMtpStorage::~MozMtpStorage() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + MTP_LOG("Storage destructed for Volume %s mStorageID 0x%08x", + mVolume->NameStr(), mStorageID); + + Volume::UnregisterVolumeObserver(this, "MozMtpStorage"); + if (mMtpStorage) { + StorageUnavailable(); + } +} + +// virtual +void +MozMtpStorage::Notify(Volume* const& aVolume) +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + if (aVolume != mVolume) { + // Not our volume + return; + } + Volume::STATE volState = aVolume->State(); + + MTP_LOG("Volume %s mStorageID 0x%08x state changed to %s SharingEnabled: %d", + aVolume->NameStr(), mStorageID, aVolume->StateStr(), + aVolume->IsSharingEnabled()); + + // vol->IsSharingEnabled really only applies to UMS volumes. We assume that + // that as long as MTP is enabled, then all volumes will be shared. The UI + // currently doesn't give us anything more granular than on/off. + + if (mMtpStorage) { + if (volState != nsIVolume::STATE_MOUNTED) { + // The volume is no longer accessible. We need to remove this storage + // from the MTP server + StorageUnavailable(); + } + } else { + if (volState == nsIVolume::STATE_MOUNTED) { + // The volume is accessible. Tell the MTP server. + StorageAvailable(); + } + } +} + +void +MozMtpStorage::StorageAvailable() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + nsCString mountPoint = mVolume->MountPoint(); + + MTP_LOG("Adding Volume %s mStorageID 0x%08x mountPoint %s to MozMtpDatabase", + mVolume->NameStr(), mStorageID, mountPoint.get()); + + RefPtr<MozMtpDatabase> db = mMozMtpServer->GetMozMtpDatabase(); + db->AddStorage(mStorageID, mountPoint.get(), mVolume->NameStr()); + + MOZ_ASSERT(!mMtpStorage); + + //TODO: Figure out what to do about maxFileSize. + + mMtpStorage.reset(new MtpStorage(mStorageID, // id + mountPoint.get(), // filePath + mVolume->NameStr(), // description + 1024uLL * 1024uLL, // reserveSpace + mVolume->IsHotSwappable(), // removable + 2uLL * 1024uLL * 1024uLL * 1024uLL)); // maxFileSize + RefPtr<RefCountedMtpServer> server = mMozMtpServer->GetMtpServer(); + + MTP_LOG("Adding Volume %s mStorageID 0x%08x mountPoint %s to MtpServer", + mVolume->NameStr(), mStorageID, mountPoint.get()); + server->addStorage(mMtpStorage.get()); +} + +void +MozMtpStorage::StorageUnavailable() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + MOZ_ASSERT(mMtpStorage); + + MTP_LOG("Removing mStorageID 0x%08x from MtpServer", mStorageID); + + RefPtr<RefCountedMtpServer> server = mMozMtpServer->GetMtpServer(); + server->removeStorage(mMtpStorage.get()); + + MTP_LOG("Removing mStorageID 0x%08x from MozMtpDatabse", mStorageID); + + RefPtr<MozMtpDatabase> db = mMozMtpServer->GetMozMtpDatabase(); + db->RemoveStorage(mStorageID); + + mMtpStorage = nullptr; +} + +END_MTP_NAMESPACE + + diff --git a/dom/system/gonk/MozMtpStorage.h b/dom/system/gonk/MozMtpStorage.h new file mode 100644 index 000000000..18d1e04ac --- /dev/null +++ b/dom/system/gonk/MozMtpStorage.h @@ -0,0 +1,47 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_mozmtpstorage_h__ +#define mozilla_system_mozmtpstorage_h__ + +#include "MozMtpCommon.h" + +#include "mozilla/UniquePtr.h" + +#include "Volume.h" + +BEGIN_MTP_NAMESPACE +using namespace android; + +class MozMtpServer; + +class MozMtpStorage : public Volume::EventObserver +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MozMtpStorage) + + MozMtpStorage(Volume* aVolume, MozMtpServer* aMozMtpServer); + + typedef nsTArray<RefPtr<MozMtpStorage> > Array; + +private: + virtual ~MozMtpStorage(); + virtual void Notify(Volume* const& aEvent); + + void StorageAvailable(); + void StorageUnavailable(); + + RefPtr<MozMtpServer> mMozMtpServer; + UniquePtr<MtpStorage> mMtpStorage; + RefPtr<Volume> mVolume; + MtpStorageID mStorageID; +}; + +END_MTP_NAMESPACE + +#endif // mozilla_system_mozmtpstorage_h__ + + diff --git a/dom/system/gonk/NetIdManager.cpp b/dom/system/gonk/NetIdManager.cpp new file mode 100644 index 000000000..510ec8b22 --- /dev/null +++ b/dom/system/gonk/NetIdManager.cpp @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "NetIdManager.h" + +NetIdManager::NetIdManager() + : mNextNetId(MIN_NET_ID) +{ +} + +int NetIdManager::getNextNetId() +{ + // Modified from + // http://androidxref.com/5.0.0_r2/xref/frameworks/base/services/ + // core/java/com/android/server/ConnectivityService.java#764 + + int netId = mNextNetId; + if (++mNextNetId > MAX_NET_ID) { + mNextNetId = MIN_NET_ID; + } + + return netId; +} + +void NetIdManager::acquire(const nsString& aInterfaceName, + NetIdInfo* aNetIdInfo) +{ + // Lookup or create one. + if (!mInterfaceToNetIdHash.Get(aInterfaceName, aNetIdInfo)) { + aNetIdInfo->mNetId = getNextNetId(); + aNetIdInfo->mRefCnt = 1; + } else { + aNetIdInfo->mRefCnt++; + } + + // Update hash and return. + mInterfaceToNetIdHash.Put(aInterfaceName, *aNetIdInfo); + + return; +} + +bool NetIdManager::lookup(const nsString& aInterfaceName, + NetIdInfo* aNetIdInfo) +{ + return mInterfaceToNetIdHash.Get(aInterfaceName, aNetIdInfo); +} + +bool NetIdManager::release(const nsString& aInterfaceName, + NetIdInfo* aNetIdInfo) +{ + if (!mInterfaceToNetIdHash.Get(aInterfaceName, aNetIdInfo)) { + return false; // No such key. + } + + aNetIdInfo->mRefCnt--; + + // Update the hash if still be referenced. + if (aNetIdInfo->mRefCnt > 0) { + mInterfaceToNetIdHash.Put(aInterfaceName, *aNetIdInfo); + return true; + } + + // No longer be referenced. Remove the entry. + mInterfaceToNetIdHash.Remove(aInterfaceName); + + return true; +}
\ No newline at end of file diff --git a/dom/system/gonk/NetIdManager.h b/dom/system/gonk/NetIdManager.h new file mode 100644 index 000000000..e35d0ecd2 --- /dev/null +++ b/dom/system/gonk/NetIdManager.h @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NetIdManager_h +#define NetIdManager_h + +#include "nsString.h" +#include "nsDataHashtable.h" + +// NetId is a logical network identifier defined by netd. +// A network is typically a physical one (i.e. PhysicalNetwork.cpp) +// for netd but it could be a virtual network as well. +// We currently use physical network only and use one-to-one +// network-interface mapping. + +class NetIdManager { +public: + // keep in sync with system/netd/NetworkController.cpp + enum { + MIN_NET_ID = 100, + MAX_NET_ID = 65535, + }; + + // We need to count the number of references since different + // application like data and mms may use the same interface. + struct NetIdInfo { + int mNetId; + int mRefCnt; + }; + +public: + NetIdManager(); + + bool lookup(const nsString& aInterfaceName, NetIdInfo* aNetIdInfo); + void acquire(const nsString& aInterfaceName, NetIdInfo* aNetIdInfo); + bool release(const nsString& aInterfaceName, NetIdInfo* aNetIdInfo); + +private: + int getNextNetId(); + int mNextNetId; + nsDataHashtable<nsStringHashKey, NetIdInfo> mInterfaceToNetIdHash; +}; + +#endif
\ No newline at end of file diff --git a/dom/system/gonk/NetworkInterfaceListService.js b/dom/system/gonk/NetworkInterfaceListService.js new file mode 100644 index 000000000..62fe046aa --- /dev/null +++ b/dom/system/gonk/NetworkInterfaceListService.js @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const NETWORKLISTSERVICE_CONTRACTID = + "@mozilla.org/network/interface-list-service;1"; +const NETWORKLISTSERVICE_CID = + Components.ID("{3780be6e-7012-4e53-ade6-15212fb88a0d}"); + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsISyncMessageSender"); + +function NetworkInterfaceListService () { +} + +NetworkInterfaceListService.prototype = { + classID: NETWORKLISTSERVICE_CID, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInterfaceListService]), + + getDataInterfaceList: function(aConditions) { + return new NetworkInterfaceList( + cpmm.sendSyncMessage( + 'NetworkInterfaceList:ListInterface', + { + excludeSupl: (aConditions & + Ci.nsINetworkInterfaceListService. + LIST_NOT_INCLUDE_SUPL_INTERFACES) != 0, + excludeMms: (aConditions & + Ci.nsINetworkInterfaceListService. + LIST_NOT_INCLUDE_MMS_INTERFACES) != 0, + excludeIms: (aConditions & + Ci.nsINetworkInterfaceListService. + LIST_NOT_INCLUDE_IMS_INTERFACES) != 0, + excludeDun: (aConditions & + Ci.nsINetworkInterfaceListService. + LIST_NOT_INCLUDE_DUN_INTERFACES) != 0, + excludeFota: (aConditions & + Ci.nsINetworkInterfaceListService. + LIST_NOT_INCLUDE_FOTA_INTERFACES) != 0 + } + )[0]); + } +}; + +function FakeNetworkInfo(aAttributes) { + this.state = aAttributes.state; + this.type = aAttributes.type; + this.name = aAttributes.name; + this.ips = aAttributes.ips; + this.prefixLengths = aAttributes.prefixLengths; + this.gateways = aAttributes.gateways; + this.dnses = aAttributes.dnses; +} +FakeNetworkInfo.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInfo]), + + getAddresses: function (ips, prefixLengths) { + ips.value = this.ips.slice(); + prefixLengths.value = this.prefixLengths.slice(); + + return this.ips.length; + }, + + getGateways: function (count) { + if (count) { + count.value = this.gateways.length; + } + return this.gateways.slice(); + }, + + getDnses: function (count) { + if (count) { + count.value = this.dnses.length; + } + return this.dnses.slice(); + } +}; + +function NetworkInterfaceList (aInterfaceLiterals) { + this._interfaces = []; + for (let entry of aInterfaceLiterals) { + this._interfaces.push(new FakeNetworkInfo(entry)); + } +} + +NetworkInterfaceList.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInterfaceList]), + + getNumberOfInterface: function() { + return this._interfaces.length; + }, + + getInterfaceInfo: function(index) { + if (!this._interfaces) { + return null; + } + return this._interfaces[index]; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkInterfaceListService]); + diff --git a/dom/system/gonk/NetworkInterfaceListService.manifest b/dom/system/gonk/NetworkInterfaceListService.manifest new file mode 100644 index 000000000..a827e778f --- /dev/null +++ b/dom/system/gonk/NetworkInterfaceListService.manifest @@ -0,0 +1,17 @@ +# Copyright 2012 Mozilla Foundation and Mozilla contributors +# +# 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. + +# NetworkInterfaceListService.js +component {3780be6e-7012-4e53-ade6-15212fb88a0d} NetworkInterfaceListService.js +contract @mozilla.org/network/interface-list-service;1 {3780be6e-7012-4e53-ade6-15212fb88a0d} diff --git a/dom/system/gonk/NetworkManager.js b/dom/system/gonk/NetworkManager.js new file mode 100644 index 000000000..9d7a5683e --- /dev/null +++ b/dom/system/gonk/NetworkManager.js @@ -0,0 +1,1219 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/systemlibs.js"); +Cu.import("resource://gre/modules/Promise.jsm"); + +const NETWORKMANAGER_CONTRACTID = "@mozilla.org/network/manager;1"; +const NETWORKMANAGER_CID = + Components.ID("{1ba9346b-53b5-4660-9dc6-58f0b258d0a6}"); + +const DEFAULT_PREFERRED_NETWORK_TYPE = Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET; + +XPCOMUtils.defineLazyGetter(this, "ppmm", function() { + return Cc["@mozilla.org/parentprocessmessagemanager;1"] + .getService(Ci.nsIMessageBroadcaster); +}); + +XPCOMUtils.defineLazyServiceGetter(this, "gDNSService", + "@mozilla.org/network/dns-service;1", + "nsIDNSService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkService", + "@mozilla.org/network/service;1", + "nsINetworkService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gTetheringService", + "@mozilla.org/tethering/service;1", + "nsITetheringService"); + +const TOPIC_INTERFACE_REGISTERED = "network-interface-registered"; +const TOPIC_INTERFACE_UNREGISTERED = "network-interface-unregistered"; +const TOPIC_ACTIVE_CHANGED = "network-active-changed"; +const TOPIC_PREF_CHANGED = "nsPref:changed"; +const TOPIC_XPCOM_SHUTDOWN = "xpcom-shutdown"; +const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed"; +const PREF_MANAGE_OFFLINE_STATUS = "network.gonk.manage-offline-status"; +const PREF_NETWORK_DEBUG_ENABLED = "network.debugging.enabled"; + +const IPV4_ADDRESS_ANY = "0.0.0.0"; +const IPV6_ADDRESS_ANY = "::0"; + +const IPV4_MAX_PREFIX_LENGTH = 32; +const IPV6_MAX_PREFIX_LENGTH = 128; + +// Connection Type for Network Information API +const CONNECTION_TYPE_CELLULAR = 0; +const CONNECTION_TYPE_BLUETOOTH = 1; +const CONNECTION_TYPE_ETHERNET = 2; +const CONNECTION_TYPE_WIFI = 3; +const CONNECTION_TYPE_OTHER = 4; +const CONNECTION_TYPE_NONE = 5; + +const MANUAL_PROXY_CONFIGURATION = 1; + +var debug; +function updateDebug() { + let debugPref = false; // set default value here. + try { + debugPref = debugPref || Services.prefs.getBoolPref(PREF_NETWORK_DEBUG_ENABLED); + } catch (e) {} + + if (debugPref) { + debug = function(s) { + dump("-*- NetworkManager: " + s + "\n"); + }; + } else { + debug = function(s) {}; + } +} +updateDebug(); + +function defineLazyRegExp(obj, name, pattern) { + obj.__defineGetter__(name, function() { + delete obj[name]; + return obj[name] = new RegExp(pattern); + }); +} + +function ExtraNetworkInfo(aNetwork) { + let ips = {}; + let prefixLengths = {}; + aNetwork.info.getAddresses(ips, prefixLengths); + + this.state = aNetwork.info.state; + this.type = aNetwork.info.type; + this.name = aNetwork.info.name; + this.ips = ips.value; + this.prefixLengths = prefixLengths.value; + this.gateways = aNetwork.info.getGateways(); + this.dnses = aNetwork.info.getDnses(); + this.httpProxyHost = aNetwork.httpProxyHost; + this.httpProxyPort = aNetwork.httpProxyPort; + this.mtu = aNetwork.mtu; +} +ExtraNetworkInfo.prototype = { + getAddresses: function(aIps, aPrefixLengths) { + aIps.value = this.ips.slice(); + aPrefixLengths.value = this.prefixLengths.slice(); + + return this.ips.length; + }, + + getGateways: function(aCount) { + if (aCount) { + aCount.value = this.gateways.length; + } + + return this.gateways.slice(); + }, + + getDnses: function(aCount) { + if (aCount) { + aCount.value = this.dnses.length; + } + + return this.dnses.slice(); + } +}; + +function NetworkInterfaceLinks() +{ + this.resetLinks(); +} +NetworkInterfaceLinks.prototype = { + linkRoutes: null, + gateways: null, + interfaceName: null, + extraRoutes: null, + + setLinks: function(linkRoutes, gateways, interfaceName) { + this.linkRoutes = linkRoutes; + this.gateways = gateways; + this.interfaceName = interfaceName; + }, + + resetLinks: function() { + this.linkRoutes = []; + this.gateways = []; + this.interfaceName = ""; + this.extraRoutes = []; + }, + + compareGateways: function(gateways) { + if (this.gateways.length != gateways.length) { + return false; + } + + for (let i = 0; i < this.gateways.length; i++) { + if (this.gateways[i] != gateways[i]) { + return false; + } + } + + return true; + } +}; + +/** + * This component watches for network interfaces changing state and then + * adjusts routes etc. accordingly. + */ +function NetworkManager() { + this.networkInterfaces = {}; + this.networkInterfaceLinks = {}; + + try { + this._manageOfflineStatus = + Services.prefs.getBoolPref(PREF_MANAGE_OFFLINE_STATUS); + } catch(ex) { + // Ignore. + } + Services.prefs.addObserver(PREF_MANAGE_OFFLINE_STATUS, this, false); + Services.prefs.addObserver(PREF_NETWORK_DEBUG_ENABLED, this, false); + Services.obs.addObserver(this, TOPIC_XPCOM_SHUTDOWN, false); + + this.setAndConfigureActive(); + + ppmm.addMessageListener('NetworkInterfaceList:ListInterface', this); + + // Used in resolveHostname(). + defineLazyRegExp(this, "REGEXP_IPV4", "^\\d{1,3}(?:\\.\\d{1,3}){3}$"); + defineLazyRegExp(this, "REGEXP_IPV6", "^[\\da-fA-F]{4}(?::[\\da-fA-F]{4}){7}$"); +} +NetworkManager.prototype = { + classID: NETWORKMANAGER_CID, + classInfo: XPCOMUtils.generateCI({classID: NETWORKMANAGER_CID, + contractID: NETWORKMANAGER_CONTRACTID, + classDescription: "Network Manager", + interfaces: [Ci.nsINetworkManager]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkManager, + Ci.nsISupportsWeakReference, + Ci.nsIObserver, + Ci.nsISettingsServiceCallback]), + + // nsIObserver + + observe: function(subject, topic, data) { + switch (topic) { + case TOPIC_PREF_CHANGED: + if (data === PREF_NETWORK_DEBUG_ENABLED) { + updateDebug(); + } else if (data === PREF_MANAGE_OFFLINE_STATUS) { + this._manageOfflineStatus = + Services.prefs.getBoolPref(PREF_MANAGE_OFFLINE_STATUS); + debug(PREF_MANAGE_OFFLINE_STATUS + " has changed to " + this._manageOfflineStatus); + } + break; + case TOPIC_XPCOM_SHUTDOWN: + Services.obs.removeObserver(this, TOPIC_XPCOM_SHUTDOWN); + Services.prefs.removeObserver(PREF_MANAGE_OFFLINE_STATUS, this); + Services.prefs.removeObserver(PREF_NETWORK_DEBUG_ENABLED, this); + break; + } + }, + + receiveMessage: function(aMsg) { + switch (aMsg.name) { + case "NetworkInterfaceList:ListInterface": { + let excludeMms = aMsg.json.excludeMms; + let excludeSupl = aMsg.json.excludeSupl; + let excludeIms = aMsg.json.excludeIms; + let excludeDun = aMsg.json.excludeDun; + let excludeFota = aMsg.json.excludeFota; + let interfaces = []; + + for (let key in this.networkInterfaces) { + let network = this.networkInterfaces[key]; + let i = network.info; + if ((i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS && excludeMms) || + (i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_SUPL && excludeSupl) || + (i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_IMS && excludeIms) || + (i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN && excludeDun) || + (i.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_FOTA && excludeFota)) { + continue; + } + + let ips = {}; + let prefixLengths = {}; + i.getAddresses(ips, prefixLengths); + + interfaces.push({ + state: i.state, + type: i.type, + name: i.name, + ips: ips.value, + prefixLengths: prefixLengths.value, + gateways: i.getGateways(), + dnses: i.getDnses() + }); + } + return interfaces; + } + } + }, + + getNetworkId: function(aNetworkInfo) { + let id = "device"; + try { + if (aNetworkInfo instanceof Ci.nsIRilNetworkInfo) { + let rilInfo = aNetworkInfo.QueryInterface(Ci.nsIRilNetworkInfo); + id = "ril" + rilInfo.serviceId; + } + } catch (e) {} + + return id + "-" + aNetworkInfo.type; + }, + + // nsINetworkManager + + registerNetworkInterface: function(network) { + if (!(network instanceof Ci.nsINetworkInterface)) { + throw Components.Exception("Argument must be nsINetworkInterface.", + Cr.NS_ERROR_INVALID_ARG); + } + let networkId = this.getNetworkId(network.info); + if (networkId in this.networkInterfaces) { + throw Components.Exception("Network with that type already registered!", + Cr.NS_ERROR_INVALID_ARG); + } + this.networkInterfaces[networkId] = network; + this.networkInterfaceLinks[networkId] = new NetworkInterfaceLinks(); + + Services.obs.notifyObservers(network.info, TOPIC_INTERFACE_REGISTERED, null); + debug("Network '" + networkId + "' registered."); + }, + + _addSubnetRoutes: function(network) { + let ips = {}; + let prefixLengths = {}; + let length = network.getAddresses(ips, prefixLengths); + let promises = []; + + for (let i = 0; i < length; i++) { + debug('Adding subnet routes: ' + ips.value[i] + '/' + prefixLengths.value[i]); + promises.push( + gNetworkService.modifyRoute(Ci.nsINetworkService.MODIFY_ROUTE_ADD, + network.name, ips.value[i], prefixLengths.value[i]) + .catch(aError => { + debug("_addSubnetRoutes error: " + aError); + })); + } + + return Promise.all(promises); + }, + + updateNetworkInterface: function(network) { + if (!(network instanceof Ci.nsINetworkInterface)) { + throw Components.Exception("Argument must be nsINetworkInterface.", + Cr.NS_ERROR_INVALID_ARG); + } + let networkId = this.getNetworkId(network.info); + if (!(networkId in this.networkInterfaces)) { + throw Components.Exception("No network with that type registered.", + Cr.NS_ERROR_INVALID_ARG); + } + debug("Network " + network.info.type + "/" + network.info.name + + " changed state to " + network.info.state); + + // Keep a copy of network in case it is modified while we are updating. + let extNetworkInfo = new ExtraNetworkInfo(network); + + // Note that since Lollipop we need to allocate and initialize + // something through netd, so we add createNetwork/destroyNetwork + // to deal with that explicitly. + + switch (extNetworkInfo.state) { + case Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED: + + this._createNetwork(extNetworkInfo.name) + // Remove pre-created default route and let setAndConfigureActive() + // to set default route only on preferred network + .then(() => this._removeDefaultRoute(extNetworkInfo)) + // Set DNS server as early as possible to prevent from + // premature domain name lookup. + .then(() => this._setDNS(extNetworkInfo)) + .then(() => { + // Add host route for data calls + if (!this.isNetworkTypeMobile(extNetworkInfo.type)) { + return; + } + + let currentInterfaceLinks = this.networkInterfaceLinks[networkId]; + let newLinkRoutes = extNetworkInfo.getDnses().concat( + extNetworkInfo.httpProxyHost); + // If gateways have changed, remove all old routes first. + return this._handleGateways(networkId, extNetworkInfo.getGateways()) + .then(() => this._updateRoutes(currentInterfaceLinks.linkRoutes, + newLinkRoutes, + extNetworkInfo.getGateways(), + extNetworkInfo.name)) + .then(() => currentInterfaceLinks.setLinks(newLinkRoutes, + extNetworkInfo.getGateways(), + extNetworkInfo.name)); + }) + .then(() => { + if (extNetworkInfo.type != + Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN) { + return; + } + // Dun type is a special case where we add the default route to a + // secondary table. + return this.setSecondaryDefaultRoute(extNetworkInfo); + }) + .then(() => this._addSubnetRoutes(extNetworkInfo)) + .then(() => { + if (extNetworkInfo.mtu <= 0) { + return; + } + + return this._setMtu(extNetworkInfo); + }) + .then(() => this.setAndConfigureActive()) + .then(() => { + // Update data connection when Wifi connected/disconnected + if (extNetworkInfo.type == + Ci.nsINetworkInfo.NETWORK_TYPE_WIFI && this.mRil) { + for (let i = 0; i < this.mRil.numRadioInterfaces; i++) { + this.mRil.getRadioInterface(i).updateRILNetworkInterface(); + } + } + + // Probing the public network accessibility after routing table is ready + CaptivePortalDetectionHelper + .notify(CaptivePortalDetectionHelper.EVENT_CONNECT, + this.activeNetworkInfo); + }) + .then(() => { + // Notify outer modules like MmsService to start the transaction after + // the configuration of the network interface is done. + Services.obs.notifyObservers(network.info, + TOPIC_CONNECTION_STATE_CHANGED, + this.convertConnectionType(network.info)); + }) + .catch(aError => { + debug("updateNetworkInterface error: " + aError); + }); + break; + case Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED: + Promise.resolve() + .then(() => { + if (!this.isNetworkTypeMobile(extNetworkInfo.type)) { + return; + } + // Remove host route for data calls + return this._cleanupAllHostRoutes(networkId); + }) + .then(() => { + if (extNetworkInfo.type != + Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN) { + return; + } + // Remove secondary default route for dun. + return this.removeSecondaryDefaultRoute(extNetworkInfo); + }) + .then(() => { + if (extNetworkInfo.type == Ci.nsINetworkInfo.NETWORK_TYPE_WIFI || + extNetworkInfo.type == Ci.nsINetworkInfo.NETWORK_TYPE_ETHERNET) { + // Remove routing table in /proc/net/route + return this._resetRoutingTable(extNetworkInfo.name); + } + if (extNetworkInfo.type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE) { + return this._removeDefaultRoute(extNetworkInfo) + } + }) + .then(() => { + // Clear http proxy on active network. + if (this.activeNetworkInfo && + extNetworkInfo.type == this.activeNetworkInfo.type) { + this.clearNetworkProxy(); + } + + // Abort ongoing captive portal detection on the wifi interface + CaptivePortalDetectionHelper + .notify(CaptivePortalDetectionHelper.EVENT_DISCONNECT, extNetworkInfo); + }) + .then(() => this.setAndConfigureActive()) + .then(() => { + // Update data connection when Wifi connected/disconnected + if (extNetworkInfo.type == + Ci.nsINetworkInfo.NETWORK_TYPE_WIFI && this.mRil) { + for (let i = 0; i < this.mRil.numRadioInterfaces; i++) { + this.mRil.getRadioInterface(i).updateRILNetworkInterface(); + } + } + }) + .then(() => this._destroyNetwork(extNetworkInfo.name)) + .then(() => { + // Notify outer modules like MmsService to start the transaction after + // the configuration of the network interface is done. + Services.obs.notifyObservers(network.info, + TOPIC_CONNECTION_STATE_CHANGED, + this.convertConnectionType(network.info)); + }) + .catch(aError => { + debug("updateNetworkInterface error: " + aError); + }); + break; + } + }, + + unregisterNetworkInterface: function(network) { + if (!(network instanceof Ci.nsINetworkInterface)) { + throw Components.Exception("Argument must be nsINetworkInterface.", + Cr.NS_ERROR_INVALID_ARG); + } + let networkId = this.getNetworkId(network.info); + if (!(networkId in this.networkInterfaces)) { + throw Components.Exception("No network with that type registered.", + Cr.NS_ERROR_INVALID_ARG); + } + + // This is for in case a network gets unregistered without being + // DISCONNECTED. + if (this.isNetworkTypeMobile(network.info.type)) { + this._cleanupAllHostRoutes(networkId); + } + + delete this.networkInterfaces[networkId]; + + Services.obs.notifyObservers(network.info, TOPIC_INTERFACE_UNREGISTERED, null); + debug("Network '" + networkId + "' unregistered."); + }, + + _manageOfflineStatus: true, + + networkInterfaces: null, + + networkInterfaceLinks: null, + + _networkTypePriorityList: [Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET, + Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, + Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE], + get networkTypePriorityList() { + return this._networkTypePriorityList; + }, + set networkTypePriorityList(val) { + if (val.length != this._networkTypePriorityList.length) { + throw "Priority list length should equal to " + + this._networkTypePriorityList.length; + } + + // Check if types in new priority list are valid and also make sure there + // are no duplicate types. + let list = [Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET, + Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, + Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE]; + while (list.length) { + let type = list.shift(); + if (val.indexOf(type) == -1) { + throw "There is missing network type"; + } + } + + this._networkTypePriorityList = val; + }, + + getPriority: function(type) { + if (this._networkTypePriorityList.indexOf(type) == -1) { + // 0 indicates the lowest priority. + return 0; + } + + return this._networkTypePriorityList.length - + this._networkTypePriorityList.indexOf(type); + }, + + get allNetworkInfo() { + let allNetworkInfo = {}; + + for (let networkId in this.networkInterfaces) { + if (this.networkInterfaces.hasOwnProperty(networkId)) { + allNetworkInfo[networkId] = this.networkInterfaces[networkId].info; + } + } + + return allNetworkInfo; + }, + + _preferredNetworkType: DEFAULT_PREFERRED_NETWORK_TYPE, + get preferredNetworkType() { + return this._preferredNetworkType; + }, + set preferredNetworkType(val) { + if ([Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, + Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, + Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET].indexOf(val) == -1) { + throw "Invalid network type"; + } + this._preferredNetworkType = val; + }, + + _activeNetwork: null, + + get activeNetworkInfo() { + return this._activeNetwork && this._activeNetwork.info; + }, + + _overriddenActive: null, + + overrideActive: function(network) { + if ([Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, + Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, + Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET].indexOf(val) == -1) { + throw "Invalid network type"; + } + + this._overriddenActive = network; + this.setAndConfigureActive(); + }, + + _updateRoutes: function(oldLinks, newLinks, gateways, interfaceName) { + // Returns items that are in base but not in target. + function getDifference(base, target) { + return base.filter(function(i) { return target.indexOf(i) < 0; }); + } + + let addedLinks = getDifference(newLinks, oldLinks); + let removedLinks = getDifference(oldLinks, newLinks); + + if (addedLinks.length === 0 && removedLinks.length === 0) { + return Promise.resolve(); + } + + return this._setHostRoutes(false, removedLinks, interfaceName, gateways) + .then(this._setHostRoutes(true, addedLinks, interfaceName, gateways)); + }, + + _setHostRoutes: function(doAdd, ipAddresses, networkName, gateways) { + let getMaxPrefixLength = (aIp) => { + return aIp.match(this.REGEXP_IPV4) ? IPV4_MAX_PREFIX_LENGTH : IPV6_MAX_PREFIX_LENGTH; + } + + let promises = []; + + ipAddresses.forEach((aIpAddress) => { + let gateway = this.selectGateway(gateways, aIpAddress); + if (gateway) { + promises.push((doAdd) + ? gNetworkService.modifyRoute(Ci.nsINetworkService.MODIFY_ROUTE_ADD, + networkName, aIpAddress, + getMaxPrefixLength(aIpAddress), gateway) + : gNetworkService.modifyRoute(Ci.nsINetworkService.MODIFY_ROUTE_REMOVE, + networkName, aIpAddress, + getMaxPrefixLength(aIpAddress), gateway)); + } + }); + + return Promise.all(promises); + }, + + isValidatedNetwork: function(aNetworkInfo) { + let isValid = false; + try { + isValid = (this.getNetworkId(aNetworkInfo) in this.networkInterfaces); + } catch (e) { + debug("Invalid network interface: " + e); + } + + return isValid; + }, + + addHostRoute: function(aNetworkInfo, aHost) { + if (!this.isValidatedNetwork(aNetworkInfo)) { + return Promise.reject("Invalid network info."); + } + + return this.resolveHostname(aNetworkInfo, aHost) + .then((ipAddresses) => { + let promises = []; + let networkId = this.getNetworkId(aNetworkInfo); + + ipAddresses.forEach((aIpAddress) => { + let promise = + this._setHostRoutes(true, [aIpAddress], aNetworkInfo.name, aNetworkInfo.getGateways()) + .then(() => this.networkInterfaceLinks[networkId].extraRoutes.push(aIpAddress)); + + promises.push(promise); + }); + + return Promise.all(promises); + }); + }, + + removeHostRoute: function(aNetworkInfo, aHost) { + if (!this.isValidatedNetwork(aNetworkInfo)) { + return Promise.reject("Invalid network info."); + } + + return this.resolveHostname(aNetworkInfo, aHost) + .then((ipAddresses) => { + let promises = []; + let networkId = this.getNetworkId(aNetworkInfo); + + ipAddresses.forEach((aIpAddress) => { + let found = this.networkInterfaceLinks[networkId].extraRoutes.indexOf(aIpAddress); + if (found < 0) { + return; // continue + } + + let promise = + this._setHostRoutes(false, [aIpAddress], aNetworkInfo.name, aNetworkInfo.getGateways()) + .then(() => { + this.networkInterfaceLinks[networkId].extraRoutes.splice(found, 1); + }, () => { + // We should remove it even if the operation failed. + this.networkInterfaceLinks[networkId].extraRoutes.splice(found, 1); + }); + promises.push(promise); + }); + + return Promise.all(promises); + }); + }, + + isNetworkTypeSecondaryMobile: function(type) { + return (type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS || + type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_SUPL || + type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_IMS || + type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN || + type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_FOTA); + }, + + isNetworkTypeMobile: function(type) { + return (type == Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE || + this.isNetworkTypeSecondaryMobile(type)); + }, + + _handleGateways: function(networkId, gateways) { + let currentNetworkLinks = this.networkInterfaceLinks[networkId]; + if (currentNetworkLinks.gateways.length == 0 || + currentNetworkLinks.compareGateways(gateways)) { + return Promise.resolve(); + } + + let currentExtraRoutes = currentNetworkLinks.extraRoutes; + return this._cleanupAllHostRoutes(networkId) + .then(() => { + // If gateways have changed, re-add extra host routes with new gateways. + if (currentExtraRoutes.length > 0) { + this._setHostRoutes(true, + currentExtraRoutes, + currentNetworkLinks.interfaceName, + gateways) + .then(() => { + currentNetworkLinks.extraRoutes = currentExtraRoutes; + }); + } + }); + }, + + _cleanupAllHostRoutes: function(networkId) { + let currentNetworkLinks = this.networkInterfaceLinks[networkId]; + let hostRoutes = currentNetworkLinks.linkRoutes.concat( + currentNetworkLinks.extraRoutes); + + if (hostRoutes.length === 0) { + return Promise.resolve(); + } + + return this._setHostRoutes(false, + hostRoutes, + currentNetworkLinks.interfaceName, + currentNetworkLinks.gateways) + .catch((aError) => { + debug("Error (" + aError + ") on _cleanupAllHostRoutes, keep proceeding."); + }) + .then(() => currentNetworkLinks.resetLinks()); + }, + + selectGateway: function(gateways, host) { + for (let i = 0; i < gateways.length; i++) { + let gateway = gateways[i]; + if (gateway.match(this.REGEXP_IPV4) && host.match(this.REGEXP_IPV4) || + gateway.indexOf(":") != -1 && host.indexOf(":") != -1) { + return gateway; + } + } + return null; + }, + + _setSecondaryRoute: function(aDoAdd, aInterfaceName, aRoute) { + return new Promise((aResolve, aReject) => { + if (aDoAdd) { + gNetworkService.addSecondaryRoute(aInterfaceName, aRoute, + (aSuccess) => { + if (!aSuccess) { + aReject("addSecondaryRoute failed"); + return; + } + aResolve(); + }); + } else { + gNetworkService.removeSecondaryRoute(aInterfaceName, aRoute, + (aSuccess) => { + if (!aSuccess) { + debug("removeSecondaryRoute failed") + } + // Always resolve. + aResolve(); + }); + } + }); + }, + + setSecondaryDefaultRoute: function(network) { + let gateways = network.getGateways(); + let promises = []; + + for (let i = 0; i < gateways.length; i++) { + let isIPv6 = (gateways[i].indexOf(":") != -1) ? true : false; + // First, we need to add a host route to the gateway in the secondary + // routing table to make the gateway reachable. Host route takes the max + // prefix and gateway address 'any'. + let hostRoute = { + ip: gateways[i], + prefix: isIPv6 ? IPV6_MAX_PREFIX_LENGTH : IPV4_MAX_PREFIX_LENGTH, + gateway: isIPv6 ? IPV6_ADDRESS_ANY : IPV4_ADDRESS_ANY + }; + // Now we can add the default route through gateway. Default route takes the + // min prefix and destination ip 'any'. + let defaultRoute = { + ip: isIPv6 ? IPV6_ADDRESS_ANY : IPV4_ADDRESS_ANY, + prefix: 0, + gateway: gateways[i] + }; + + let promise = this._setSecondaryRoute(true, network.name, hostRoute) + .then(() => this._setSecondaryRoute(true, network.name, defaultRoute)); + + promises.push(promise); + } + + return Promise.all(promises); + }, + + removeSecondaryDefaultRoute: function(network) { + let gateways = network.getGateways(); + let promises = []; + + for (let i = 0; i < gateways.length; i++) { + let isIPv6 = (gateways[i].indexOf(":") != -1) ? true : false; + // Remove both default route and host route. + let defaultRoute = { + ip: isIPv6 ? IPV6_ADDRESS_ANY : IPV4_ADDRESS_ANY, + prefix: 0, + gateway: gateways[i] + }; + let hostRoute = { + ip: gateways[i], + prefix: isIPv6 ? IPV6_MAX_PREFIX_LENGTH : IPV4_MAX_PREFIX_LENGTH, + gateway: isIPv6 ? IPV6_ADDRESS_ANY : IPV4_ADDRESS_ANY + }; + + let promise = this._setSecondaryRoute(false, network.name, defaultRoute) + .then(() => this._setSecondaryRoute(false, network.name, hostRoute)); + + promises.push(promise); + } + + return Promise.all(promises); + }, + + /** + * Determine the active interface and configure it. + */ + setAndConfigureActive: function() { + debug("Evaluating whether active network needs to be changed."); + let oldActive = this._activeNetwork; + + if (this._overriddenActive) { + debug("We have an override for the active network: " + + this._overriddenActive.info.name); + // The override was just set, so reconfigure the network. + if (this._activeNetwork != this._overriddenActive) { + this._activeNetwork = this._overriddenActive; + this._setDefaultRouteAndProxy(this._activeNetwork, oldActive); + Services.obs.notifyObservers(this.activeNetworkInfo, + TOPIC_ACTIVE_CHANGED, null); + } + return; + } + + // The active network is already our preferred type. + if (this.activeNetworkInfo && + this.activeNetworkInfo.state == Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED && + this.activeNetworkInfo.type == this._preferredNetworkType) { + debug("Active network is already our preferred type."); + return this._setDefaultRouteAndProxy(this._activeNetwork, oldActive); + } + + // Find a suitable network interface to activate. + this._activeNetwork = null; + let anyConnected = false; + + for (let key in this.networkInterfaces) { + let network = this.networkInterfaces[key]; + if (network.info.state != Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + continue; + } + anyConnected = true; + + // Set active only for default connections. + if (network.info.type != Ci.nsINetworkInfo.NETWORK_TYPE_WIFI && + network.info.type != Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE && + network.info.type != Ci.nsINetworkInfo.NETWORK_TYPE_ETHERNET) { + continue; + } + + if (network.info.type == this.preferredNetworkType) { + this._activeNetwork = network; + debug("Found our preferred type of network: " + network.info.name); + break; + } + + // Initialize the active network with the first connected network. + if (!this._activeNetwork) { + this._activeNetwork = network; + continue; + } + + // Compare the prioriy between two network types. If found incoming + // network with higher priority, replace the active network. + if (this.getPriority(this._activeNetwork.type) < this.getPriority(network.type)) { + this._activeNetwork = network; + } + } + + return Promise.resolve() + .then(() => { + if (!this._activeNetwork) { + return Promise.resolve(); + } + + return this._setDefaultRouteAndProxy(this._activeNetwork, oldActive); + }) + .then(() => { + if (this._activeNetwork != oldActive) { + Services.obs.notifyObservers(this.activeNetworkInfo, + TOPIC_ACTIVE_CHANGED, null); + } + + if (this._manageOfflineStatus) { + Services.io.offline = !anyConnected && + (gTetheringService.state === + Ci.nsITetheringService.TETHERING_STATE_INACTIVE); + } + }); + }, + + resolveHostname: function(aNetworkInfo, aHostname) { + // Sanity check for null, undefined and empty string... etc. + if (!aHostname) { + return Promise.reject(new Error("hostname is empty: " + aHostname)); + } + + if (aHostname.match(this.REGEXP_IPV4) || + aHostname.match(this.REGEXP_IPV6)) { + return Promise.resolve([aHostname]); + } + + // Wrap gDNSService.asyncResolveExtended to a promise, which + // resolves with an array of ip addresses or rejects with + // the reason otherwise. + let hostResolveWrapper = aNetId => { + return new Promise((aResolve, aReject) => { + // Callback for gDNSService.asyncResolveExtended. + let onLookupComplete = (aRequest, aRecord, aStatus) => { + if (!Components.isSuccessCode(aStatus)) { + aReject(new Error("Failed to resolve '" + aHostname + + "', with status: " + aStatus)); + return; + } + + let retval = []; + while (aRecord.hasMore()) { + retval.push(aRecord.getNextAddrAsString()); + } + + if (!retval.length) { + aReject(new Error("No valid address after DNS lookup!")); + return; + } + + debug("hostname is resolved: " + aHostname); + debug("Addresses: " + JSON.stringify(retval)); + + aResolve(retval); + }; + + debug('Calling gDNSService.asyncResolveExtended: ' + aNetId + ', ' + aHostname); + gDNSService.asyncResolveExtended(aHostname, + 0, + aNetId, + onLookupComplete, + Services.tm.mainThread); + }); + }; + + // TODO: |getNetId| will be implemented as a sync call in nsINetworkManager + // once Bug 1141903 is landed. + return gNetworkService.getNetId(aNetworkInfo.name) + .then(aNetId => hostResolveWrapper(aNetId)); + }, + + convertConnectionType: function(aNetworkInfo) { + // If there is internal interface change (e.g., MOBILE_MMS, MOBILE_SUPL), + // the function will return null so that it won't trigger type change event + // in NetworkInformation API. + if (aNetworkInfo.type != Ci.nsINetworkInterface.NETWORK_TYPE_WIFI && + aNetworkInfo.type != Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE && + aNetworkInfo.type != Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET) { + return null; + } + + if (aNetworkInfo.state == Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED) { + return CONNECTION_TYPE_NONE; + } + + switch (aNetworkInfo.type) { + case Ci.nsINetworkInfo.NETWORK_TYPE_WIFI: + return CONNECTION_TYPE_WIFI; + case Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE: + return CONNECTION_TYPE_CELLULAR; + case Ci.nsINetworkInterface.NETWORK_TYPE_ETHERNET: + return CONNECTION_TYPE_ETHERNET; + } + }, + + _setDNS: function(aNetworkInfo) { + return new Promise((aResolve, aReject) => { + let dnses = aNetworkInfo.getDnses(); + let gateways = aNetworkInfo.getGateways(); + gNetworkService.setDNS(aNetworkInfo.name, dnses.length, dnses, + gateways.length, gateways, (aError) => { + if (aError) { + aReject("setDNS failed"); + return; + } + aResolve(); + }); + }); + }, + + _setMtu: function(aNetworkInfo) { + return new Promise((aResolve, aReject) => { + gNetworkService.setMtu(aNetworkInfo.name, aNetworkInfo.mtu, (aSuccess) => { + if (!aSuccess) { + debug("setMtu failed"); + } + // Always resolve. + aResolve(); + }); + }); + }, + + _createNetwork: function(aInterfaceName) { + return new Promise((aResolve, aReject) => { + gNetworkService.createNetwork(aInterfaceName, (aSuccess) => { + if (!aSuccess) { + aReject("createNetwork failed"); + return; + } + aResolve(); + }); + }); + }, + + _destroyNetwork: function(aInterfaceName) { + return new Promise((aResolve, aReject) => { + gNetworkService.destroyNetwork(aInterfaceName, (aSuccess) => { + if (!aSuccess) { + debug("destroyNetwork failed") + } + // Always resolve. + aResolve(); + }); + }); + }, + + _resetRoutingTable: function(aInterfaceName) { + return new Promise((aResolve, aReject) => { + gNetworkService.resetRoutingTable(aInterfaceName, (aSuccess) => { + if (!aSuccess) { + debug("resetRoutingTable failed"); + } + // Always resolve. + aResolve(); + }); + }); + }, + + _removeDefaultRoute: function(aNetworkInfo) { + return new Promise((aResolve, aReject) => { + let gateways = aNetworkInfo.getGateways(); + gNetworkService.removeDefaultRoute(aNetworkInfo.name, gateways.length, + gateways, (aSuccess) => { + if (!aSuccess) { + debug("removeDefaultRoute failed"); + } + // Always resolve. + aResolve(); + }); + }); + }, + + _setDefaultRouteAndProxy: function(aNetwork, aOldNetwork) { + if (aOldNetwork) { + return this._removeDefaultRoute(aOldNetwork.info) + .then(() => this._setDefaultRouteAndProxy(aNetwork, null)); + } + + return new Promise((aResolve, aReject) => { + let networkInfo = aNetwork.info; + let gateways = networkInfo.getGateways(); + + gNetworkService.setDefaultRoute(networkInfo.name, gateways.length, gateways, + (aSuccess) => { + if (!aSuccess) { + gNetworkService.destroyNetwork(networkInfo.name, function() { + aReject("setDefaultRoute failed"); + }); + return; + } + this.setNetworkProxy(aNetwork); + aResolve(); + }); + }); + }, + + setNetworkProxy: function(aNetwork) { + try { + if (!aNetwork.httpProxyHost || aNetwork.httpProxyHost === "") { + // Sets direct connection to internet. + this.clearNetworkProxy(); + + debug("No proxy support for " + aNetwork.info.name + " network interface."); + return; + } + + debug("Going to set proxy settings for " + aNetwork.info.name + " network interface."); + // Sets manual proxy configuration. + Services.prefs.setIntPref("network.proxy.type", MANUAL_PROXY_CONFIGURATION); + + // Do not use this proxy server for all protocols. + Services.prefs.setBoolPref("network.proxy.share_proxy_settings", false); + Services.prefs.setCharPref("network.proxy.http", aNetwork.httpProxyHost); + Services.prefs.setCharPref("network.proxy.ssl", aNetwork.httpProxyHost); + let port = aNetwork.httpProxyPort === 0 ? 8080 : aNetwork.httpProxyPort; + Services.prefs.setIntPref("network.proxy.http_port", port); + Services.prefs.setIntPref("network.proxy.ssl_port", port); + } catch(ex) { + debug("Exception " + ex + ". Unable to set proxy setting for " + + aNetwork.info.name + " network interface."); + } + }, + + clearNetworkProxy: function() { + debug("Going to clear all network proxy."); + + Services.prefs.clearUserPref("network.proxy.type"); + Services.prefs.clearUserPref("network.proxy.share_proxy_settings"); + Services.prefs.clearUserPref("network.proxy.http"); + Services.prefs.clearUserPref("network.proxy.http_port"); + Services.prefs.clearUserPref("network.proxy.ssl"); + Services.prefs.clearUserPref("network.proxy.ssl_port"); + }, +}; + +var CaptivePortalDetectionHelper = (function() { + + const EVENT_CONNECT = "Connect"; + const EVENT_DISCONNECT = "Disconnect"; + let _ongoingInterface = null; + let _available = ("nsICaptivePortalDetector" in Ci); + let getService = function() { + return Cc['@mozilla.org/toolkit/captive-detector;1'] + .getService(Ci.nsICaptivePortalDetector); + }; + + let _performDetection = function(interfaceName, callback) { + let capService = getService(); + let capCallback = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsICaptivePortalCallback]), + prepare: function() { + capService.finishPreparation(interfaceName); + }, + complete: function(success) { + _ongoingInterface = null; + callback(success); + } + }; + + // Abort any unfinished captive portal detection. + if (_ongoingInterface != null) { + capService.abort(_ongoingInterface); + _ongoingInterface = null; + } + try { + capService.checkCaptivePortal(interfaceName, capCallback); + _ongoingInterface = interfaceName; + } catch (e) { + debug('Fail to detect captive portal due to: ' + e.message); + } + }; + + let _abort = function(interfaceName) { + if (_ongoingInterface !== interfaceName) { + return; + } + + let capService = getService(); + capService.abort(_ongoingInterface); + _ongoingInterface = null; + }; + + return { + EVENT_CONNECT: EVENT_CONNECT, + EVENT_DISCONNECT: EVENT_DISCONNECT, + notify: function(eventType, network) { + switch (eventType) { + case EVENT_CONNECT: + // perform captive portal detection on wifi interface + if (_available && network && + network.type == Ci.nsINetworkInfo.NETWORK_TYPE_WIFI) { + _performDetection(network.name, function() { + // TODO: bug 837600 + // We can disconnect wifi in here if user abort the login procedure. + }); + } + + break; + case EVENT_DISCONNECT: + if (_available && + network.type == Ci.nsINetworkInfo.NETWORK_TYPE_WIFI) { + _abort(network.name); + } + break; + } + } + }; +}()); + +XPCOMUtils.defineLazyGetter(NetworkManager.prototype, "mRil", function() { + try { + return Cc["@mozilla.org/ril;1"].getService(Ci.nsIRadioInterfaceLayer); + } catch (e) {} + + return null; +}); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkManager]); diff --git a/dom/system/gonk/NetworkManager.manifest b/dom/system/gonk/NetworkManager.manifest new file mode 100644 index 000000000..995fa6559 --- /dev/null +++ b/dom/system/gonk/NetworkManager.manifest @@ -0,0 +1,3 @@ +# NetworkManager.js +component {1ba9346b-53b5-4660-9dc6-58f0b258d0a6} NetworkManager.js +contract @mozilla.org/network/manager;1 {1ba9346b-53b5-4660-9dc6-58f0b258d0a6} diff --git a/dom/system/gonk/NetworkService.js b/dom/system/gonk/NetworkService.js new file mode 100644 index 000000000..7147f40c7 --- /dev/null +++ b/dom/system/gonk/NetworkService.js @@ -0,0 +1,862 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +const NETWORKSERVICE_CONTRACTID = "@mozilla.org/network/service;1"; +const NETWORKSERVICE_CID = Components.ID("{48c13741-aec9-4a86-8962-432011708261}"); + +const TOPIC_PREF_CHANGED = "nsPref:changed"; +const TOPIC_XPCOM_SHUTDOWN = "xpcom-shutdown"; +const PREF_NETWORK_DEBUG_ENABLED = "network.debugging.enabled"; + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkWorker", + "@mozilla.org/network/worker;1", + "nsINetworkWorker"); + +// 1xx - Requested action is proceeding +const NETD_COMMAND_PROCEEDING = 100; +// 2xx - Requested action has been successfully completed +const NETD_COMMAND_OKAY = 200; +// 4xx - The command is accepted but the requested action didn't +// take place. +const NETD_COMMAND_FAIL = 400; +// 5xx - The command syntax or parameters error +const NETD_COMMAND_ERROR = 500; +// 6xx - Unsolicited broadcasts +const NETD_COMMAND_UNSOLICITED = 600; + +const WIFI_CTRL_INTERFACE = "wl0.1"; + +var debug; +function updateDebug() { + let debugPref = false; // set default value here. + try { + debugPref = debugPref || Services.prefs.getBoolPref(PREF_NETWORK_DEBUG_ENABLED); + } catch (e) {} + + if (debugPref) { + debug = function(s) { + dump("-*- NetworkService: " + s + "\n"); + }; + } else { + debug = function(s) {}; + } +} +updateDebug(); + +function netdResponseType(aCode) { + return Math.floor(aCode / 100) * 100; +} + +function isError(aCode) { + let type = netdResponseType(aCode); + return (type !== NETD_COMMAND_PROCEEDING && type !== NETD_COMMAND_OKAY); +} + +function Task(aId, aParams, aSetupFunction) { + this.id = aId; + this.params = aParams; + this.setupFunction = aSetupFunction; +} + +function NetworkWorkerRequestQueue(aNetworkService) { + this.networkService = aNetworkService; + this.tasks = []; +} +NetworkWorkerRequestQueue.prototype = { + runQueue: function() { + if (this.tasks.length === 0) { + return; + } + + let task = this.tasks[0]; + debug("run task id: " + task.id); + + if (typeof task.setupFunction === 'function') { + // If setupFunction returns false, skip sending to Network Worker but call + // handleWorkerMessage() directly with task id, as if the response was + // returned from Network Worker. + if (!task.setupFunction()) { + this.networkService.handleWorkerMessage({id: task.id}); + return; + } + } + + gNetworkWorker.postMessage(task.params); + }, + + enqueue: function(aId, aParams, aSetupFunction) { + debug("enqueue id: " + aId); + this.tasks.push(new Task(aId, aParams, aSetupFunction)); + + if (this.tasks.length === 1) { + this.runQueue(); + } + }, + + dequeue: function(aId) { + debug("dequeue id: " + aId); + + if (!this.tasks.length || this.tasks[0].id != aId) { + debug("Id " + aId + " is not on top of the queue"); + return; + } + + this.tasks.shift(); + if (this.tasks.length > 0) { + // Run queue on the next tick. + Services.tm.currentThread.dispatch(() => { + this.runQueue(); + }, Ci.nsIThread.DISPATCH_NORMAL); + } + } +}; + + +/** + * This component watches for network interfaces changing state and then + * adjusts routes etc. accordingly. + */ +function NetworkService() { + debug("Starting NetworkService."); + + let self = this; + + if (gNetworkWorker) { + let networkListener = { + onEvent: function(aEvent) { + self.handleWorkerMessage(aEvent); + } + }; + gNetworkWorker.start(networkListener); + } + // Callbacks to invoke when a reply arrives from the net_worker. + this.controlCallbacks = Object.create(null); + + this.addedRoutes = new Map(); + this.netWorkerRequestQueue = new NetworkWorkerRequestQueue(this); + this.shutdown = false; + + Services.prefs.addObserver(PREF_NETWORK_DEBUG_ENABLED, this, false); + Services.obs.addObserver(this, TOPIC_XPCOM_SHUTDOWN, false); +} + +NetworkService.prototype = { + classID: NETWORKSERVICE_CID, + classInfo: XPCOMUtils.generateCI({classID: NETWORKSERVICE_CID, + contractID: NETWORKSERVICE_CONTRACTID, + classDescription: "Network Service", + interfaces: [Ci.nsINetworkService]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkService, + Ci.nsIObserver]), + + addedRoutes: null, + + shutdown: false, + + // nsIObserver + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case TOPIC_PREF_CHANGED: + if (aData === PREF_NETWORK_DEBUG_ENABLED) { + updateDebug(); + } + break; + case TOPIC_XPCOM_SHUTDOWN: + debug("NetworkService shutdown"); + this.shutdown = true; + if (gNetworkWorker) { + gNetworkWorker.shutdown(); + gNetworkWorker = null; + } + + Services.obs.removeObserver(this, TOPIC_XPCOM_SHUTDOWN); + Services.prefs.removeObserver(PREF_NETWORK_DEBUG_ENABLED, this); + break; + } + }, + + // Helpers + + idgen: 0, + controlMessage: function(aParams, aCallback, aSetupFunction) { + if (this.shutdown) { + return; + } + + let id = this.idgen++; + aParams.id = id; + if (aCallback) { + this.controlCallbacks[id] = aCallback; + } + + // For now, we use aSetupFunction to determine if this command needs to be + // queued or not. + if (aSetupFunction) { + this.netWorkerRequestQueue.enqueue(id, aParams, aSetupFunction); + return; + } + + if (gNetworkWorker) { + gNetworkWorker.postMessage(aParams); + } + }, + + handleWorkerMessage: function(aResponse) { + debug("NetworkManager received message from worker: " + JSON.stringify(aResponse)); + let id = aResponse.id; + if (aResponse.broadcast === true) { + Services.obs.notifyObservers(null, aResponse.topic, aResponse.reason); + return; + } + let callback = this.controlCallbacks[id]; + if (callback) { + callback.call(this, aResponse); + delete this.controlCallbacks[id]; + } + + this.netWorkerRequestQueue.dequeue(id); + }, + + // nsINetworkService + + getNetworkInterfaceStats: function(aInterfaceName, aCallback) { + debug("getNetworkInterfaceStats for " + aInterfaceName); + + let file = new FileUtils.File("/proc/net/dev"); + if (!file) { + aCallback.networkStatsAvailable(false, 0, 0, Date.now()); + return; + } + + NetUtil.asyncFetch({ + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true + }, function(inputStream, status) { + let rxBytes = 0, + txBytes = 0, + now = Date.now(); + + if (Components.isSuccessCode(status)) { + // Find record for corresponding interface. + let statExpr = /(\S+): +(\d+) +\d+ +\d+ +\d+ +\d+ +\d+ +\d+ +\d+ +(\d+) +\d+ +\d+ +\d+ +\d+ +\d+ +\d+ +\d+/; + let data = + NetUtil.readInputStreamToString(inputStream, inputStream.available()) + .split("\n"); + for (let i = 2; i < data.length; i++) { + let parseResult = statExpr.exec(data[i]); + if (parseResult && parseResult[1] === aInterfaceName) { + rxBytes = parseInt(parseResult[2], 10); + txBytes = parseInt(parseResult[3], 10); + break; + } + } + } + + // netd always return success even interface doesn't exist. + aCallback.networkStatsAvailable(true, rxBytes, txBytes, now); + }); + }, + + setNetworkTetheringAlarm(aEnable, aInterface) { + // Method called when enabling disabling tethering, it checks if there is + // some alarm active and move from interfaceAlarm to globalAlarm because + // interfaceAlarm doens't work in tethering scenario due to forwarding. + debug("setNetworkTetheringAlarm for tethering" + aEnable); + + let filename = aEnable ? "/proc/net/xt_quota/" + aInterface + "Alert" : + "/proc/net/xt_quota/globalAlert"; + + let file = new FileUtils.File(filename); + if (!file) { + return; + } + + NetUtil.asyncFetch({ + uri: NetUtil.newURI(file), + loadUsingSystemPrincipal: true + }, (inputStream, status) => { + if (Components.isSuccessCode(status)) { + let data = NetUtil.readInputStreamToString(inputStream, inputStream.available()) + .split("\n"); + if (data) { + let threshold = parseInt(data[0], 10); + + this._setNetworkTetheringAlarm(aEnable, aInterface, threshold); + } + } + }); + }, + + _setNetworkTetheringAlarm(aEnable, aInterface, aThreshold, aCallback) { + debug("_setNetworkTetheringAlarm for tethering" + aEnable); + + let cmd = aEnable ? "setTetheringAlarm" : "removeTetheringAlarm"; + + let params = { + cmd: cmd, + ifname: aInterface, + threshold: aThreshold, + }; + + this.controlMessage(params, function(aData) { + let code = aData.resultCode; + let reason = aData.resultReason; + let enableString = aEnable ? "Enable" : "Disable"; + debug(enableString + " tethering Alarm result: Code " + code + " reason " + reason); + if (aCallback) { + aCallback.networkUsageAlarmResult(null); + } + }); + }, + + setNetworkInterfaceAlarm: function(aInterfaceName, aThreshold, aCallback) { + if (!aInterfaceName) { + aCallback.networkUsageAlarmResult(-1); + return; + } + + let self = this; + this._disableNetworkInterfaceAlarm(aInterfaceName, function(aResult) { + if (aThreshold < 0) { + if (!isError(aResult.resultCode)) { + aCallback.networkUsageAlarmResult(null); + return; + } + aCallback.networkUsageAlarmResult(aResult.reason); + return + } + + // Check if tethering is enabled + let params = { + cmd: "getTetheringStatus" + }; + + self.controlMessage(params, function(aResult) { + if (isError(aResult.resultCode)) { + aCallback.networkUsageAlarmResult(aResult.reason); + return; + } + + if (aResult.resultReason.indexOf('started') == -1) { + // Tethering disabled, set interfaceAlarm + self._setNetworkInterfaceAlarm(aInterfaceName, aThreshold, aCallback); + return; + } + + // Tethering enabled, set globalAlarm + self._setNetworkTetheringAlarm(true, aInterfaceName, aThreshold, aCallback); + }); + }); + }, + + _setNetworkInterfaceAlarm: function(aInterfaceName, aThreshold, aCallback) { + debug("setNetworkInterfaceAlarm for " + aInterfaceName + " at " + aThreshold + "bytes"); + + let params = { + cmd: "setNetworkInterfaceAlarm", + ifname: aInterfaceName, + threshold: aThreshold + }; + + params.report = true; + + this.controlMessage(params, function(aResult) { + if (!isError(aResult.resultCode)) { + aCallback.networkUsageAlarmResult(null); + return; + } + + this._enableNetworkInterfaceAlarm(aInterfaceName, aThreshold, aCallback); + }); + }, + + _enableNetworkInterfaceAlarm: function(aInterfaceName, aThreshold, aCallback) { + debug("enableNetworkInterfaceAlarm for " + aInterfaceName + " at " + aThreshold + "bytes"); + + let params = { + cmd: "enableNetworkInterfaceAlarm", + ifname: aInterfaceName, + threshold: aThreshold + }; + + params.report = true; + + this.controlMessage(params, function(aResult) { + if (!isError(aResult.resultCode)) { + aCallback.networkUsageAlarmResult(null); + return; + } + aCallback.networkUsageAlarmResult(aResult.reason); + }); + }, + + _disableNetworkInterfaceAlarm: function(aInterfaceName, aCallback) { + debug("disableNetworkInterfaceAlarm for " + aInterfaceName); + + let params = { + cmd: "disableNetworkInterfaceAlarm", + ifname: aInterfaceName, + }; + + params.report = true; + + this.controlMessage(params, function(aResult) { + aCallback(aResult); + }); + }, + + setWifiOperationMode: function(aInterfaceName, aMode, aCallback) { + debug("setWifiOperationMode on " + aInterfaceName + " to " + aMode); + + let params = { + cmd: "setWifiOperationMode", + ifname: aInterfaceName, + mode: aMode + }; + + params.report = true; + + this.controlMessage(params, function(aResult) { + if (isError(aResult.resultCode)) { + aCallback.wifiOperationModeResult("netd command error"); + } else { + aCallback.wifiOperationModeResult(null); + } + }); + }, + + resetRoutingTable: function(aInterfaceName, aCallback) { + let options = { + cmd: "removeNetworkRoute", + ifname: aInterfaceName + }; + + this.controlMessage(options, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + setDNS: function(aInterfaceName, aDnsesCount, aDnses, aGatewaysCount, + aGateways, aCallback) { + debug("Going to set DNS to " + aInterfaceName); + let options = { + cmd: "setDNS", + ifname: aInterfaceName, + domain: "mozilla." + aInterfaceName + ".domain", + dnses: aDnses, + gateways: aGateways + }; + this.controlMessage(options, function(aResult) { + aCallback.setDnsResult(aResult.success ? null : aResult.reason); + }); + }, + + setDefaultRoute: function(aInterfaceName, aCount, aGateways, aCallback) { + debug("Going to change default route to " + aInterfaceName); + let options = { + cmd: "setDefaultRoute", + ifname: aInterfaceName, + gateways: aGateways + }; + this.controlMessage(options, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + removeDefaultRoute: function(aInterfaceName, aCount, aGateways, aCallback) { + debug("Remove default route for " + aInterfaceName); + let options = { + cmd: "removeDefaultRoute", + ifname: aInterfaceName, + gateways: aGateways + }; + this.controlMessage(options, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + _routeToString: function(aInterfaceName, aHost, aPrefixLength, aGateway) { + return aHost + "-" + aPrefixLength + "-" + aGateway + "-" + aInterfaceName; + }, + + modifyRoute: function(aAction, aInterfaceName, aHost, aPrefixLength, aGateway) { + let command; + + switch (aAction) { + case Ci.nsINetworkService.MODIFY_ROUTE_ADD: + command = 'addHostRoute'; + break; + case Ci.nsINetworkService.MODIFY_ROUTE_REMOVE: + command = 'removeHostRoute'; + break; + default: + debug('Unknown action: ' + aAction); + return Promise.reject(); + } + + let route = this._routeToString(aInterfaceName, aHost, aPrefixLength, aGateway); + let setupFunc = () => { + let count = this.addedRoutes.get(route); + debug(command + ": " + route + " -> " + count); + + // Return false if there is no need to send the command to network worker. + if ((aAction == Ci.nsINetworkService.MODIFY_ROUTE_ADD && count) || + (aAction == Ci.nsINetworkService.MODIFY_ROUTE_REMOVE && + (!count || count > 1))) { + return false; + } + + return true; + }; + + debug(command + " " + aHost + " on " + aInterfaceName); + let options = { + cmd: command, + ifname: aInterfaceName, + gateway: aGateway, + prefixLength: aPrefixLength, + ip: aHost + }; + + return new Promise((aResolve, aReject) => { + this.controlMessage(options, (aData) => { + let count = this.addedRoutes.get(route); + + // Remove route from addedRoutes on success or failure. + if (aAction == Ci.nsINetworkService.MODIFY_ROUTE_REMOVE) { + if (count > 1) { + this.addedRoutes.set(route, count - 1); + } else { + this.addedRoutes.delete(route); + } + } + + if (aData.error) { + aReject(aData.reason); + return; + } + + if (aAction == Ci.nsINetworkService.MODIFY_ROUTE_ADD) { + this.addedRoutes.set(route, count ? count + 1 : 1); + } + + aResolve(); + }, setupFunc); + }); + }, + + addSecondaryRoute: function(aInterfaceName, aRoute, aCallback) { + debug("Going to add route to secondary table on " + aInterfaceName); + let options = { + cmd: "addSecondaryRoute", + ifname: aInterfaceName, + ip: aRoute.ip, + prefix: aRoute.prefix, + gateway: aRoute.gateway + }; + this.controlMessage(options, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + removeSecondaryRoute: function(aInterfaceName, aRoute, aCallback) { + debug("Going to remove route from secondary table on " + aInterfaceName); + let options = { + cmd: "removeSecondaryRoute", + ifname: aInterfaceName, + ip: aRoute.ip, + prefix: aRoute.prefix, + gateway: aRoute.gateway + }; + this.controlMessage(options, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + // Enable/Disable DHCP server. + setDhcpServer: function(aEnabled, aConfig, aCallback) { + if (null === aConfig) { + aConfig = {}; + } + + aConfig.cmd = "setDhcpServer"; + aConfig.enabled = aEnabled; + + this.controlMessage(aConfig, function(aResponse) { + if (!aResponse.success) { + aCallback.dhcpServerResult('Set DHCP server error'); + return; + } + aCallback.dhcpServerResult(null); + }); + }, + + // Enable/disable WiFi tethering by sending commands to netd. + setWifiTethering: function(aEnable, aConfig, aCallback) { + // config should've already contained: + // .ifname + // .internalIfname + // .externalIfname + aConfig.wifictrlinterfacename = WIFI_CTRL_INTERFACE; + aConfig.cmd = "setWifiTethering"; + + // The callback function in controlMessage may not be fired immediately. + this.controlMessage(aConfig, (aData) => { + let code = aData.resultCode; + let reason = aData.resultReason; + let enable = aData.enable; + let enableString = aEnable ? "Enable" : "Disable"; + + debug(enableString + " Wifi tethering result: Code " + code + " reason " + reason); + + this.setNetworkTetheringAlarm(aEnable, aConfig.externalIfname); + + if (isError(code)) { + aCallback.wifiTetheringEnabledChange("netd command error"); + } else { + aCallback.wifiTetheringEnabledChange(null); + } + }); + }, + + // Enable/disable USB tethering by sending commands to netd. + setUSBTethering: function(aEnable, aConfig, aCallback) { + aConfig.cmd = "setUSBTethering"; + // The callback function in controlMessage may not be fired immediately. + this.controlMessage(aConfig, (aData) => { + let code = aData.resultCode; + let reason = aData.resultReason; + let enable = aData.enable; + let enableString = aEnable ? "Enable" : "Disable"; + + debug(enableString + " USB tethering result: Code " + code + " reason " + reason); + + this.setNetworkTetheringAlarm(aEnable, aConfig.externalIfname); + + if (isError(code)) { + aCallback.usbTetheringEnabledChange("netd command error"); + } else { + aCallback.usbTetheringEnabledChange(null); + } + }); + }, + + // Switch usb function by modifying property of persist.sys.usb.config. + enableUsbRndis: function(aEnable, aCallback) { + debug("enableUsbRndis: " + aEnable); + + let params = { + cmd: "enableUsbRndis", + enable: aEnable + }; + // Ask net work to report the result when this value is set to true. + if (aCallback) { + params.report = true; + } else { + params.report = false; + } + + // The callback function in controlMessage may not be fired immediately. + //this._usbTetheringAction = TETHERING_STATE_ONGOING; + this.controlMessage(params, function(aData) { + aCallback.enableUsbRndisResult(aData.result, aData.enable); + }); + }, + + updateUpStream: function(aPrevious, aCurrent, aCallback) { + let params = { + cmd: "updateUpStream", + preInternalIfname: aPrevious.internalIfname, + preExternalIfname: aPrevious.externalIfname, + curInternalIfname: aCurrent.internalIfname, + curExternalIfname: aCurrent.externalIfname + }; + + this.controlMessage(params, function(aData) { + let code = aData.resultCode; + let reason = aData.resultReason; + debug("updateUpStream result: Code " + code + " reason " + reason); + aCallback.updateUpStreamResult(!isError(code), aData.curExternalIfname); + }); + }, + + getInterfaces: function(callback) { + let params = { + cmd: "getInterfaces", + isAsync: true + }; + + this.controlMessage(params, function(data) { + debug("getInterfaces result: " + JSON.stringify(data)); + let success = !isError(data.resultCode); + callback.getInterfacesResult(success, data.interfaceList); + }); + }, + + getInterfaceConfig: function(ifname, callback) { + let params = { + cmd: "getInterfaceConfig", + ifname: ifname, + isAsync: true + }; + + this.controlMessage(params, function(data) { + debug("getInterfaceConfig result: " + JSON.stringify(data)); + let success = !isError(data.resultCode); + let result = { ip: data.ipAddr, + prefix: data.prefixLength, + link: data.flag, + mac: data.macAddr }; + callback.getInterfaceConfigResult(success, result); + }); + }, + + setInterfaceConfig: function(config, callback) { + config.cmd = "setInterfaceConfig"; + config.isAsync = true; + + this.controlMessage(config, function(data) { + debug("setInterfaceConfig result: " + JSON.stringify(data)); + let success = !isError(data.resultCode); + callback.setInterfaceConfigResult(success); + }); + }, + + configureInterface: function(aConfig, aCallback) { + let params = { + cmd: "configureInterface", + ifname: aConfig.ifname, + ipaddr: aConfig.ipaddr, + mask: aConfig.mask, + gateway_long: aConfig.gateway, + dns1_long: aConfig.dns1, + dns2_long: aConfig.dns2, + }; + + this.controlMessage(params, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + dhcpRequest: function(aInterfaceName, aCallback) { + let params = { + cmd: "dhcpRequest", + ifname: aInterfaceName + }; + + this.controlMessage(params, function(aResult) { + aCallback.dhcpRequestResult(!aResult.error, aResult.error ? null : aResult); + }); + }, + + stopDhcp: function(aInterfaceName, aCallback) { + let params = { + cmd: "stopDhcp", + ifname: aInterfaceName + }; + + this.controlMessage(params, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + enableInterface: function(aInterfaceName, aCallback) { + let params = { + cmd: "enableInterface", + ifname: aInterfaceName + }; + + this.controlMessage(params, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + disableInterface: function(aInterfaceName, aCallback) { + let params = { + cmd: "disableInterface", + ifname: aInterfaceName + }; + + this.controlMessage(params, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + resetConnections: function(aInterfaceName, aCallback) { + let params = { + cmd: "resetConnections", + ifname: aInterfaceName + }; + + this.controlMessage(params, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + createNetwork: function(aInterfaceName, aCallback) { + let params = { + cmd: "createNetwork", + ifname: aInterfaceName + }; + + this.controlMessage(params, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + destroyNetwork: function(aInterfaceName, aCallback) { + let params = { + cmd: "destroyNetwork", + ifname: aInterfaceName + }; + + this.controlMessage(params, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + }, + + getNetId: function(aInterfaceName) { + let params = { + cmd: "getNetId", + ifname: aInterfaceName + }; + + return new Promise((aResolve, aReject) => { + this.controlMessage(params, result => { + if (result.error) { + aReject(result.reason); + return; + } + aResolve(result.netId); + }); + }); + }, + + setMtu: function (aInterfaceName, aMtu, aCallback) { + debug("Set MTU on " + aInterfaceName + ": " + aMtu); + + let params = { + cmd: "setMtu", + ifname: aInterfaceName, + mtu: aMtu + }; + + this.controlMessage(params, function(aResult) { + aCallback.nativeCommandResult(!aResult.error); + }); + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkService]); diff --git a/dom/system/gonk/NetworkService.manifest b/dom/system/gonk/NetworkService.manifest new file mode 100644 index 000000000..caf8f2554 --- /dev/null +++ b/dom/system/gonk/NetworkService.manifest @@ -0,0 +1,3 @@ +# NetworkService.js +component {48c13741-aec9-4a86-8962-432011708261} NetworkService.js +contract @mozilla.org/network/service;1 {48c13741-aec9-4a86-8962-432011708261} diff --git a/dom/system/gonk/NetworkUtils.cpp b/dom/system/gonk/NetworkUtils.cpp new file mode 100644 index 000000000..d661368b8 --- /dev/null +++ b/dom/system/gonk/NetworkUtils.cpp @@ -0,0 +1,2973 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +#include "NetworkUtils.h" + +#include "mozilla/Sprintf.h" +#include "SystemProperty.h" + +#include <android/log.h> +#include <limits> +#include "mozilla/dom/network/NetUtils.h" +#include "mozilla/fallible.h" +#include "base/task.h" + +#include <errno.h> +#include <string.h> +#include <sys/types.h> // struct addrinfo +#include <sys/socket.h> // getaddrinfo(), freeaddrinfo() +#include <netdb.h> +#include <arpa/inet.h> // inet_ntop() + +#define _DEBUG 0 + +#define WARN(args...) __android_log_print(ANDROID_LOG_WARN, "NetworkUtils", ## args) +#define ERROR(args...) __android_log_print(ANDROID_LOG_ERROR, "NetworkUtils", ## args) + +#if _DEBUG +#define NU_DBG(args...) __android_log_print(ANDROID_LOG_DEBUG, "NetworkUtils" , ## args) +#else +#define NU_DBG(args...) +#endif + +using namespace mozilla::dom; +using namespace mozilla::ipc; +using mozilla::system::Property; + +static const char* PERSIST_SYS_USB_CONFIG_PROPERTY = "persist.sys.usb.config"; +static const char* SYS_USB_CONFIG_PROPERTY = "sys.usb.config"; +static const char* SYS_USB_STATE_PROPERTY = "sys.usb.state"; + +static const char* USB_FUNCTION_RNDIS = "rndis"; +static const char* USB_FUNCTION_ADB = "adb"; + +// Use this command to continue the function chain. +static const char* DUMMY_COMMAND = "tether status"; + +// IPV6 Tethering is not supported in AOSP, use the property to +// identify vendor specific support in IPV6. We can remove this flag +// once upstream Android support IPV6 in tethering. +static const char* IPV6_TETHERING = "ro.tethering.ipv6"; + +// Retry 20 times (2 seconds) for usb state transition. +static const uint32_t USB_FUNCTION_RETRY_TIMES = 20; +// Check "sys.usb.state" every 100ms. +static const uint32_t USB_FUNCTION_RETRY_INTERVAL = 100; + +// 1xx - Requested action is proceeding +static const uint32_t NETD_COMMAND_PROCEEDING = 100; +// 2xx - Requested action has been successfully completed +static const uint32_t NETD_COMMAND_OKAY = 200; +// 4xx - The command is accepted but the requested action didn't +// take place. +static const uint32_t NETD_COMMAND_FAIL = 400; +// 5xx - The command syntax or parameters error +static const uint32_t NETD_COMMAND_ERROR = 500; +// 6xx - Unsolicited broadcasts +static const uint32_t NETD_COMMAND_UNSOLICITED = 600; + +// Broadcast messages +static const uint32_t NETD_COMMAND_INTERFACE_CHANGE = 600; +static const uint32_t NETD_COMMAND_BANDWIDTH_CONTROLLER = 601; + +static const char* INTERFACE_DELIMIT = ","; +static const char* USB_CONFIG_DELIMIT = ","; +static const char* NETD_MESSAGE_DELIMIT = " "; + +static const uint32_t BUF_SIZE = 1024; + +static const int32_t SUCCESS = 0; + +static uint32_t SDK_VERSION; +static uint32_t SUPPORT_IPV6_TETHERING; + +struct IFProperties { + char gateway[Property::VALUE_MAX_LENGTH]; + char dns1[Property::VALUE_MAX_LENGTH]; + char dns2[Property::VALUE_MAX_LENGTH]; +}; + +struct CurrentCommand { + CommandChain* chain; + CommandCallback callback; + char command[MAX_COMMAND_SIZE]; +}; + +typedef Tuple3<NetdCommand*, CommandChain*, CommandCallback> QueueData; + +#define GET_CURRENT_NETD_COMMAND (gCommandQueue.IsEmpty() ? nullptr : gCommandQueue[0].a) +#define GET_CURRENT_CHAIN (gCommandQueue.IsEmpty() ? nullptr : gCommandQueue[0].b) +#define GET_CURRENT_CALLBACK (gCommandQueue.IsEmpty() ? nullptr : gCommandQueue[0].c) +#define GET_CURRENT_COMMAND (gCommandQueue.IsEmpty() ? nullptr : gCommandQueue[0].a->mData) + +// A macro for native function call return value check. +// For native function call, non-zero return value means failure. +#define RETURN_IF_FAILED(rv) do { \ + if (SUCCESS != rv) { \ + return rv; \ + } \ +} while (0); + +#define WARN_IF_FAILED(rv) do { \ + if (SUCCESS != rv) { \ + WARN("Error (%d) occurred in %s (%s:%d)", rv, __PRETTY_FUNCTION__, __FILE__, __LINE__); \ + } \ +} while (0); + +static NetworkUtils* gNetworkUtils; +static nsTArray<QueueData> gCommandQueue; +static CurrentCommand gCurrentCommand; +static bool gPending = false; +static nsTArray<nsCString> gReason; +static NetworkParams *gWifiTetheringParms = 0; + +static nsTArray<CommandChain*> gCommandChainQueue; + +const CommandFunc NetworkUtils::sWifiEnableChain[] = { + NetworkUtils::clearWifiTetherParms, + NetworkUtils::wifiFirmwareReload, + NetworkUtils::startAccessPointDriver, + NetworkUtils::setAccessPoint, + NetworkUtils::startSoftAP, + NetworkUtils::setConfig, + NetworkUtils::tetherInterface, + NetworkUtils::addInterfaceToLocalNetwork, + NetworkUtils::addRouteToLocalNetwork, + NetworkUtils::setIpForwardingEnabled, + NetworkUtils::tetheringStatus, + NetworkUtils::startTethering, + NetworkUtils::setDnsForwarders, + NetworkUtils::enableNat, + NetworkUtils::wifiTetheringSuccess +}; + +const CommandFunc NetworkUtils::sWifiDisableChain[] = { + NetworkUtils::clearWifiTetherParms, + NetworkUtils::stopSoftAP, + NetworkUtils::stopAccessPointDriver, + NetworkUtils::wifiFirmwareReload, + NetworkUtils::untetherInterface, + NetworkUtils::removeInterfaceFromLocalNetwork, + NetworkUtils::preTetherInterfaceList, + NetworkUtils::postTetherInterfaceList, + NetworkUtils::disableNat, + NetworkUtils::setIpForwardingEnabled, + NetworkUtils::stopTethering, + NetworkUtils::wifiTetheringSuccess +}; + +const CommandFunc NetworkUtils::sWifiFailChain[] = { + NetworkUtils::clearWifiTetherParms, + NetworkUtils::stopSoftAP, + NetworkUtils::setIpForwardingEnabled, + NetworkUtils::stopTethering +}; + +const CommandFunc NetworkUtils::sWifiRetryChain[] = { + NetworkUtils::clearWifiTetherParms, + NetworkUtils::stopSoftAP, + NetworkUtils::stopTethering, + + // sWifiEnableChain: + NetworkUtils::wifiFirmwareReload, + NetworkUtils::startAccessPointDriver, + NetworkUtils::setAccessPoint, + NetworkUtils::startSoftAP, + NetworkUtils::setConfig, + NetworkUtils::tetherInterface, + NetworkUtils::addInterfaceToLocalNetwork, + NetworkUtils::addRouteToLocalNetwork, + NetworkUtils::setIpForwardingEnabled, + NetworkUtils::tetheringStatus, + NetworkUtils::startTethering, + NetworkUtils::setDnsForwarders, + NetworkUtils::enableNat, + NetworkUtils::wifiTetheringSuccess +}; + +const CommandFunc NetworkUtils::sWifiOperationModeChain[] = { + NetworkUtils::wifiFirmwareReload, + NetworkUtils::wifiOperationModeSuccess +}; + +const CommandFunc NetworkUtils::sUSBEnableChain[] = { + NetworkUtils::setConfig, + NetworkUtils::enableNat, + NetworkUtils::setIpForwardingEnabled, + NetworkUtils::tetherInterface, + NetworkUtils::addInterfaceToLocalNetwork, + NetworkUtils::addRouteToLocalNetwork, + NetworkUtils::tetheringStatus, + NetworkUtils::startTethering, + NetworkUtils::setDnsForwarders, + NetworkUtils::addUpstreamInterface, + NetworkUtils::usbTetheringSuccess +}; + +const CommandFunc NetworkUtils::sUSBDisableChain[] = { + NetworkUtils::untetherInterface, + NetworkUtils::removeInterfaceFromLocalNetwork, + NetworkUtils::preTetherInterfaceList, + NetworkUtils::postTetherInterfaceList, + NetworkUtils::removeUpstreamInterface, + NetworkUtils::disableNat, + NetworkUtils::setIpForwardingEnabled, + NetworkUtils::stopTethering, + NetworkUtils::usbTetheringSuccess +}; + +const CommandFunc NetworkUtils::sUSBFailChain[] = { + NetworkUtils::stopSoftAP, + NetworkUtils::setIpForwardingEnabled, + NetworkUtils::stopTethering +}; + +const CommandFunc NetworkUtils::sUpdateUpStreamChain[] = { + NetworkUtils::cleanUpStream, + NetworkUtils::removeUpstreamInterface, + NetworkUtils::createUpStream, + NetworkUtils::addUpstreamInterface, + NetworkUtils::updateUpStreamSuccess +}; + +const CommandFunc NetworkUtils::sStartDhcpServerChain[] = { + NetworkUtils::setConfig, + NetworkUtils::startTethering, + NetworkUtils::setDhcpServerSuccess +}; + +const CommandFunc NetworkUtils::sStopDhcpServerChain[] = { + NetworkUtils::stopTethering, + NetworkUtils::setDhcpServerSuccess +}; + +const CommandFunc NetworkUtils::sNetworkInterfaceEnableAlarmChain[] = { + NetworkUtils::enableAlarm, + NetworkUtils::setQuota, + NetworkUtils::setAlarm, + NetworkUtils::networkInterfaceAlarmSuccess +}; + +const CommandFunc NetworkUtils::sNetworkInterfaceDisableAlarmChain[] = { + NetworkUtils::removeQuota, + NetworkUtils::disableAlarm, + NetworkUtils::networkInterfaceAlarmSuccess +}; + +const CommandFunc NetworkUtils::sNetworkInterfaceSetAlarmChain[] = { + NetworkUtils::setAlarm, + NetworkUtils::networkInterfaceAlarmSuccess +}; + +const CommandFunc NetworkUtils::sGetInterfacesChain[] = { + NetworkUtils::getInterfaceList, + NetworkUtils::getInterfacesSuccess +}; + +const CommandFunc NetworkUtils::sGetInterfaceConfigChain[] = { + NetworkUtils::getConfig, + NetworkUtils::getInterfaceConfigSuccess +}; + +const CommandFunc NetworkUtils::sSetInterfaceConfigChain[] = { + NetworkUtils::setConfig, + NetworkUtils::setInterfaceConfigSuccess +}; + +const CommandFunc NetworkUtils::sTetheringInterfaceSetAlarmChain[] = { + NetworkUtils::setGlobalAlarm, + NetworkUtils::removeAlarm, + NetworkUtils::networkInterfaceAlarmSuccess +}; + +const CommandFunc NetworkUtils::sTetheringInterfaceRemoveAlarmChain[] = { + NetworkUtils::removeGlobalAlarm, + NetworkUtils::setAlarm, + NetworkUtils::networkInterfaceAlarmSuccess +}; + +const CommandFunc NetworkUtils::sTetheringGetStatusChain[] = { + NetworkUtils::tetheringStatus, + NetworkUtils::defaultAsyncSuccessHandler +}; + +/** + * Helper function to get the mask from given prefix length. + */ +static uint32_t makeMask(const uint32_t prefixLength) +{ + uint32_t mask = 0; + for (uint32_t i = 0; i < prefixLength; ++i) { + mask |= (0x80000000 >> i); + } + return ntohl(mask); +} + +/** + * Helper function to get the network part of an ip from prefix. + * param ip must be in network byte order. + */ +static char* getNetworkAddr(const uint32_t ip, const uint32_t prefix) +{ + uint32_t mask = 0, subnet = 0; + + mask = ~mask << (32 - prefix); + mask = htonl(mask); + subnet = ip & mask; + + struct in_addr addr; + addr.s_addr = subnet; + + return inet_ntoa(addr); +} + +/** + * Helper function to split string by seperator, store split result as an nsTArray. + */ +static void split(char* str, const char* sep, nsTArray<nsCString>& result) +{ + char *s = strtok(str, sep); + while (s != nullptr) { + result.AppendElement(s); + s = strtok(nullptr, sep); + } +} + +static void split(char* str, const char* sep, nsTArray<nsString>& result) +{ + char *s = strtok(str, sep); + while (s != nullptr) { + result.AppendElement(NS_ConvertUTF8toUTF16(s)); + s = strtok(nullptr, sep); + } +} + +/** + * Helper function that implement join function. + */ +static void join(nsTArray<nsCString>& array, + const char* sep, + const uint32_t maxlen, + char* result) +{ +#define CHECK_LENGTH(len, add, max) len += add; \ + if (len > max - 1) \ + return; \ + + uint32_t len = 0; + uint32_t seplen = strlen(sep); + + if (array.Length() > 0) { + CHECK_LENGTH(len, strlen(array[0].get()), maxlen) + strcpy(result, array[0].get()); + + for (uint32_t i = 1; i < array.Length(); i++) { + CHECK_LENGTH(len, seplen, maxlen) + strcat(result, sep); + + CHECK_LENGTH(len, strlen(array[i].get()), maxlen) + strcat(result, array[i].get()); + } + } + +#undef CHECK_LEN +} + +static void convertUTF8toUTF16(nsTArray<nsCString>& narrow, + nsTArray<nsString>& wide, + uint32_t length) +{ + for (uint32_t i = 0; i < length; i++) { + wide.AppendElement(NS_ConvertUTF8toUTF16(narrow[i].get())); + } +} + +/** + * Helper function to get network interface properties from the system property table. + */ +static void getIFProperties(const char* ifname, IFProperties& prop) +{ + char key[Property::KEY_MAX_LENGTH]; + snprintf(key, Property::KEY_MAX_LENGTH - 1, "net.%s.gw", ifname); + Property::Get(key, prop.gateway, ""); + snprintf(key, Property::KEY_MAX_LENGTH - 1, "net.%s.dns1", ifname); + Property::Get(key, prop.dns1, ""); + snprintf(key, Property::KEY_MAX_LENGTH - 1, "net.%s.dns2", ifname); + Property::Get(key, prop.dns2, ""); +} + +static int getIpType(const char *aIp) { + struct addrinfo hint, *ip_info = NULL; + + memset(&hint, 0, sizeof(hint)); + hint.ai_family = AF_UNSPEC; + hint.ai_flags = AI_NUMERICHOST; + + if (getaddrinfo(aIp, NULL, &hint, &ip_info)) { + return AF_UNSPEC; + } + + int type = ip_info->ai_family; + freeaddrinfo(ip_info); + + return type; +} + +static void postMessage(NetworkResultOptions& aResult) +{ + MOZ_ASSERT(gNetworkUtils); + MOZ_ASSERT(gNetworkUtils->getMessageCallback()); + + if (*(gNetworkUtils->getMessageCallback())) + (*(gNetworkUtils->getMessageCallback()))(aResult); +} + +static void postMessage(NetworkParams& aOptions, NetworkResultOptions& aResult) +{ + MOZ_ASSERT(gNetworkUtils); + MOZ_ASSERT(gNetworkUtils->getMessageCallback()); + + aResult.mId = aOptions.mId; + + if (*(gNetworkUtils->getMessageCallback())) + (*(gNetworkUtils->getMessageCallback()))(aResult); +} + +void NetworkUtils::runNextQueuedCommandChain() +{ + if (gCommandChainQueue.IsEmpty()) { + NU_DBG("No command chain left in the queue. Done!"); + return; + } + NU_DBG("Process the queued command chain."); + CommandChain* nextChain = gCommandChainQueue[0]; + NetworkResultOptions newResult; + next(nextChain, false, newResult); +} + +void NetworkUtils::next(CommandChain* aChain, bool aError, NetworkResultOptions& aResult) +{ + if (aError) { + ErrorCallback onError = aChain->getErrorCallback(); + if(onError) { + aResult.mError = true; + (*onError)(aChain->getParams(), aResult); + } + delete aChain; + gCommandChainQueue.RemoveElementAt(0); + runNextQueuedCommandChain(); + return; + } + CommandFunc f = aChain->getNextCommand(); + if (!f) { + delete aChain; + gCommandChainQueue.RemoveElementAt(0); + runNextQueuedCommandChain(); + return; + } + + (*f)(aChain, next, aResult); +} + +CommandResult::CommandResult(int32_t aResultCode) + : mIsPending(false) +{ + // This is usually not a netd command. We treat the return code + // typical linux convention, which uses 0 to indicate success. + mResult.mError = (aResultCode == SUCCESS ? false : true); + mResult.mResultCode = aResultCode; + if (aResultCode != SUCCESS) { + // The returned value is sometimes negative, make sure we pass a positive + // error number to strerror. + enum { STRERROR_R_BUF_SIZE = 1024, }; + char strerrorBuf[STRERROR_R_BUF_SIZE]; + strerror_r(abs(aResultCode), strerrorBuf, STRERROR_R_BUF_SIZE); + mResult.mReason = NS_ConvertUTF8toUTF16(strerrorBuf); + } +} + +CommandResult::CommandResult(const mozilla::dom::NetworkResultOptions& aResult) + : mResult(aResult) + , mIsPending(false) +{ +} + +CommandResult::CommandResult(const Pending&) + : mIsPending(true) +{ +} + +bool CommandResult::isPending() const +{ + return mIsPending; +} + +/** + * Send command to netd. + */ +void NetworkUtils::nextNetdCommand() +{ + if (gCommandQueue.IsEmpty() || gPending) { + return; + } + + gCurrentCommand.chain = GET_CURRENT_CHAIN; + gCurrentCommand.callback = GET_CURRENT_CALLBACK; + snprintf(gCurrentCommand.command, MAX_COMMAND_SIZE - 1, "%s", GET_CURRENT_COMMAND); + + NU_DBG("Sending \'%s\' command to netd.", gCurrentCommand.command); + SendNetdCommand(GET_CURRENT_NETD_COMMAND); + + gCommandQueue.RemoveElementAt(0); + gPending = true; +} + +/** + * Composite NetdCommand sent to netd + * + * @param aCommand Command sent to netd to execute. + * @param aChain Store command chain data, ex. command parameter. + * @param aCallback Callback function to be executed when the result of + * this command is returned from netd. + */ +void NetworkUtils::doCommand(const char* aCommand, CommandChain* aChain, CommandCallback aCallback) +{ + NU_DBG("Preparing to send \'%s\' command...", aCommand); + + NetdCommand* netdCommand = new NetdCommand(); + + // Android JB version adds sequence number to netd command. + if (SDK_VERSION >= 16) { + snprintf((char*)netdCommand->mData, MAX_COMMAND_SIZE - 1, "0 %s", aCommand); + } else { + snprintf((char*)netdCommand->mData, MAX_COMMAND_SIZE - 1, "%s", aCommand); + } + netdCommand->mSize = strlen((char*)netdCommand->mData) + 1; + + gCommandQueue.AppendElement(QueueData(netdCommand, aChain, aCallback)); + + nextNetdCommand(); +} + +/* + * Netd command function + */ +#define GET_CHAR(prop) NS_ConvertUTF16toUTF8(aChain->getParams().prop).get() +#define GET_FIELD(prop) aChain->getParams().prop + +void NetworkUtils::wifiFirmwareReload(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "softap fwreload %s %s", GET_CHAR(mIfname), GET_CHAR(mMode)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::startAccessPointDriver(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + // Skip the command for sdk version >= 16. + if (SDK_VERSION >= 16) { + aResult.mResultCode = 0; + aResult.mResultReason = NS_ConvertUTF8toUTF16(""); + aCallback(aChain, false, aResult); + return; + } + + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "softap start %s", GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::stopAccessPointDriver(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + // Skip the command for sdk version >= 16. + if (SDK_VERSION >= 16) { + aResult.mResultCode = 0; + aResult.mResultReason = NS_ConvertUTF8toUTF16(""); + aCallback(aChain, false, aResult); + return; + } + + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "softap stop %s", GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +/** + * Command format for sdk version < 16 + * Arguments: + * argv[2] - wlan interface + * argv[3] - SSID + * argv[4] - Security + * argv[5] - Key + * argv[6] - Channel + * argv[7] - Preamble + * argv[8] - Max SCB + * + * Command format for sdk version >= 16 + * Arguments: + * argv[2] - wlan interface + * argv[3] - SSID + * argv[4] - Security + * argv[5] - Key + * + * Command format for sdk version >= 18 + * Arguments: + * argv[2] - wlan interface + * argv[3] - SSID + * argv[4] - Broadcast/Hidden + * argv[5] - Channel + * argv[6] - Security + * argv[7] - Key + */ +void NetworkUtils::setAccessPoint(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + nsCString ssid(GET_CHAR(mSsid)); + nsCString key(GET_CHAR(mKey)); + + escapeQuote(ssid); + escapeQuote(key); + + if (SDK_VERSION >= 19) { + snprintf(command, MAX_COMMAND_SIZE - 1, "softap set %s \"%s\" broadcast 6 %s \"%s\"", + GET_CHAR(mIfname), + ssid.get(), + GET_CHAR(mSecurity), + key.get()); + } else if (SDK_VERSION >= 16) { + snprintf(command, MAX_COMMAND_SIZE - 1, "softap set %s \"%s\" %s \"%s\"", + GET_CHAR(mIfname), + ssid.get(), + GET_CHAR(mSecurity), + key.get()); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, "softap set %s %s \"%s\" %s \"%s\" 6 0 8", + GET_CHAR(mIfname), + GET_CHAR(mWifictrlinterfacename), + ssid.get(), + GET_CHAR(mSecurity), + key.get()); + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::cleanUpStream(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "nat disable %s %s 0", GET_CHAR(mPreInternalIfname), GET_CHAR(mPreExternalIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::createUpStream(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "nat enable %s %s 0", GET_CHAR(mCurInternalIfname), GET_CHAR(mCurExternalIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::startSoftAP(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + const char* command= "softap startap"; + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::stopSoftAP(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + const char* command= "softap stopap"; + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::clearWifiTetherParms(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + delete gWifiTetheringParms; + gWifiTetheringParms = 0; + next(aChain, false, aResult); +} + +void NetworkUtils::enableAlarm(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + const char* command= "bandwidth enable"; + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::disableAlarm(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + const char* command= "bandwidth disable"; + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::setQuota(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "bandwidth setiquota %s % " PRId64, GET_CHAR(mIfname), INT64_MAX); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::removeQuota(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "bandwidth removeiquota %s", GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::setAlarm(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "bandwidth setinterfacealert %s %lld", + GET_CHAR(mIfname), GET_FIELD(mThreshold)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::removeAlarm(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "bandwidth removeinterfacealert %s", GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::setGlobalAlarm(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + + snprintf(command, MAX_COMMAND_SIZE - 1, "bandwidth setglobalalert %lld", GET_FIELD(mThreshold)); + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::removeGlobalAlarm(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + + snprintf(command, MAX_COMMAND_SIZE - 1, "bandwidth removeglobalalert"); + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::tetherInterface(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "tether interface add %s", GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::addInterfaceToLocalNetwork(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + // Skip the command for sdk version < 20. + if (SDK_VERSION < 20) { + aResult.mResultCode = 0; + aResult.mResultReason = NS_ConvertUTF8toUTF16(""); + aCallback(aChain, false, aResult); + return; + } + + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "network interface add local %s", + GET_CHAR(mInternalIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::addRouteToLocalNetwork(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + // Skip the command for sdk version < 20. + if (SDK_VERSION < 20) { + aResult.mResultCode = 0; + aResult.mResultReason = NS_ConvertUTF8toUTF16(""); + aCallback(aChain, false, aResult); + return; + } + + char command[MAX_COMMAND_SIZE]; + uint32_t prefix = atoi(GET_CHAR(mPrefix)); + uint32_t ip = inet_addr(GET_CHAR(mIp)); + char* networkAddr = getNetworkAddr(ip, prefix); + + snprintf(command, MAX_COMMAND_SIZE - 1, "network route add local %s %s/%s", + GET_CHAR(mInternalIfname), networkAddr, GET_CHAR(mPrefix)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::preTetherInterfaceList(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + if (SDK_VERSION >= 16) { + snprintf(command, MAX_COMMAND_SIZE - 1, "tether interface list"); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, "tether interface list 0"); + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::postTetherInterfaceList(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + // Send the dummy command to continue the function chain. + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "%s", DUMMY_COMMAND); + + char buf[BUF_SIZE]; + NS_ConvertUTF16toUTF8 reason(aResult.mResultReason); + + size_t length = reason.Length() + 1 < BUF_SIZE ? reason.Length() + 1 : BUF_SIZE; + memcpy(buf, reason.get(), length); + split(buf, INTERFACE_DELIMIT, GET_FIELD(mInterfaceList)); + + doCommand(command, aChain, aCallback); +} + +bool isCommandChainIPv6(CommandChain* aChain, const char *externalInterface) { + // Check by gateway address + if (getIpType(GET_CHAR(mGateway)) == AF_INET6) { + return true; + } + + uint32_t length = GET_FIELD(mGateways).Length(); + for (uint32_t i = 0; i < length; i++) { + NS_ConvertUTF16toUTF8 autoGateway(GET_FIELD(mGateways)[i]); + if(getIpType(autoGateway.get()) == AF_INET6) { + return true; + } + } + + // Check by external inteface address + FILE *file = fopen("/proc/net/if_inet6", "r"); + if (!file) { + return false; + } + + bool isIPv6 = false; + char interface[32]; + while(fscanf(file, "%*s %*s %*s %*s %*s %32s", interface)) { + if (strcmp(interface, externalInterface) == 0) { + isIPv6 = true; + break; + } + } + + fclose(file); + return isIPv6; +} + +void NetworkUtils::addUpstreamInterface(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + nsCString interface(GET_CHAR(mExternalIfname)); + if (!interface.get()[0]) { + interface = GET_CHAR(mCurExternalIfname); + } + + if (SUPPORT_IPV6_TETHERING == 0 || !isCommandChainIPv6(aChain, interface.get())) { + aCallback(aChain, false, aResult); + return; + } + + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "tether interface add_upstream %s", + interface.get()); + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::removeUpstreamInterface(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + nsCString interface(GET_CHAR(mExternalIfname)); + if (!interface.get()[0]) { + interface = GET_CHAR(mPreExternalIfname); + } + + if (SUPPORT_IPV6_TETHERING == 0 || !isCommandChainIPv6(aChain, interface.get())) { + aCallback(aChain, false, aResult); + return; + } + + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "tether interface remove_upstream %s", + interface.get()); + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::setIpForwardingEnabled(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + + if (GET_FIELD(mEnable)) { + snprintf(command, MAX_COMMAND_SIZE - 1, "ipfwd enable"); + } else { + // Don't disable ip forwarding because others interface still need it. + // Send the dummy command to continue the function chain. + if (GET_FIELD(mInterfaceList).Length() > 1) { + snprintf(command, MAX_COMMAND_SIZE - 1, "%s", DUMMY_COMMAND); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, "ipfwd disable"); + } + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::tetheringStatus(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + const char* command= "tether status"; + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::stopTethering(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + + // Don't stop tethering because others interface still need it. + // Send the dummy to continue the function chain. + if (GET_FIELD(mInterfaceList).Length() > 1) { + snprintf(command, MAX_COMMAND_SIZE - 1, "%s", DUMMY_COMMAND); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, "tether stop"); + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::startTethering(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + + // We don't need to start tethering again. + // Send the dummy command to continue the function chain. + if (aResult.mResultReason.Find("started") != kNotFound) { + snprintf(command, MAX_COMMAND_SIZE - 1, "%s", DUMMY_COMMAND); + } else { + // If usbStartIp/usbEndIp is not valid, don't append them since + // the trailing white spaces will be parsed to extra empty args + // See: http://androidxref.com/4.3_r2.1/xref/system/core/libsysutils/src/FrameworkListener.cpp#78 + if (!GET_FIELD(mUsbStartIp).IsEmpty() && !GET_FIELD(mUsbEndIp).IsEmpty()) { + snprintf(command, MAX_COMMAND_SIZE - 1, "tether start %s %s %s %s", + GET_CHAR(mWifiStartIp), GET_CHAR(mWifiEndIp), + GET_CHAR(mUsbStartIp), GET_CHAR(mUsbEndIp)); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, "tether start %s %s", + GET_CHAR(mWifiStartIp), GET_CHAR(mWifiEndIp)); + } + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::untetherInterface(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "tether interface remove %s", GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::removeInterfaceFromLocalNetwork(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + // Skip the command for sdk version < 20. + if (SDK_VERSION < 20) { + aResult.mResultCode = 0; + aResult.mResultReason = NS_ConvertUTF8toUTF16(""); + aCallback(aChain, false, aResult); + return; + } + + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "network interface remove local %s", + GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::setDnsForwarders(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + + if (SDK_VERSION >= 20) { + snprintf(command, MAX_COMMAND_SIZE - 1, "tether dns set %d %s %s", + GET_FIELD(mNetId), GET_CHAR(mDns1), GET_CHAR(mDns2)); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, "tether dns set %s %s", + GET_CHAR(mDns1), GET_CHAR(mDns2)); + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::enableNat(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + + if (!GET_FIELD(mIp).IsEmpty() && !GET_FIELD(mPrefix).IsEmpty()) { + uint32_t prefix = atoi(GET_CHAR(mPrefix)); + uint32_t ip = inet_addr(GET_CHAR(mIp)); + char* networkAddr = getNetworkAddr(ip, prefix); + + // address/prefix will only take effect when secondary routing table exists. + snprintf(command, MAX_COMMAND_SIZE - 1, "nat enable %s %s 1 %s/%s", + GET_CHAR(mInternalIfname), GET_CHAR(mExternalIfname), networkAddr, + GET_CHAR(mPrefix)); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, "nat enable %s %s 0", + GET_CHAR(mInternalIfname), GET_CHAR(mExternalIfname)); + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::disableNat(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + + if (!GET_FIELD(mIp).IsEmpty() && !GET_FIELD(mPrefix).IsEmpty()) { + uint32_t prefix = atoi(GET_CHAR(mPrefix)); + uint32_t ip = inet_addr(GET_CHAR(mIp)); + char* networkAddr = getNetworkAddr(ip, prefix); + + snprintf(command, MAX_COMMAND_SIZE - 1, "nat disable %s %s 1 %s/%s", + GET_CHAR(mInternalIfname), GET_CHAR(mExternalIfname), networkAddr, + GET_CHAR(mPrefix)); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, "nat disable %s %s 0", + GET_CHAR(mInternalIfname), GET_CHAR(mExternalIfname)); + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::setDefaultInterface(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "resolver setdefaultif %s", GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::removeDefaultRoute(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + if (GET_FIELD(mLoopIndex) >= GET_FIELD(mGateways).Length()) { + aCallback(aChain, false, aResult); + return; + } + + char command[MAX_COMMAND_SIZE]; + nsTArray<nsString>& gateways = GET_FIELD(mGateways); + NS_ConvertUTF16toUTF8 autoGateway(gateways[GET_FIELD(mLoopIndex)]); + + int type = getIpType(autoGateway.get()); + snprintf(command, MAX_COMMAND_SIZE - 1, "network route remove %d %s %s/0 %s", + GET_FIELD(mNetId), GET_CHAR(mIfname), + type == AF_INET6 ? "::" : "0.0.0.0", autoGateway.get()); + + struct MyCallback { + static void callback(CommandCallback::CallbackType aOriginalCallback, + CommandChain* aChain, + bool aError, + mozilla::dom::NetworkResultOptions& aResult) + { + NS_ConvertUTF16toUTF8 reason(aResult.mResultReason); + NU_DBG("removeDefaultRoute's reason: %s", reason.get()); + if (aError && !reason.EqualsASCII("removeRoute() failed (No such process)")) { + return aOriginalCallback(aChain, aError, aResult); + } + + GET_FIELD(mLoopIndex)++; + return removeDefaultRoute(aChain, aOriginalCallback, aResult); + } + }; + + CommandCallback wrappedCallback(MyCallback::callback, aCallback); + doCommand(command, aChain, wrappedCallback); +} + +void NetworkUtils::setInterfaceDns(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + int written; + + if (SDK_VERSION >= 20) { + written = SprintfLiteral(command, "resolver setnetdns %d %s", + GET_FIELD(mNetId), GET_CHAR(mDomain)); + } else { + written = SprintfLiteral(command, "resolver setifdns %s %s", + GET_CHAR(mIfname), GET_CHAR(mDomain)); + } + + nsTArray<nsString>& dnses = GET_FIELD(mDnses); + uint32_t length = dnses.Length(); + + for (uint32_t i = 0; i < length; i++) { + NS_ConvertUTF16toUTF8 autoDns(dnses[i]); + + int ret = snprintf(command + written, sizeof(command) - written, " %s", autoDns.get()); + if (ret <= 1) { + command[written] = '\0'; + continue; + } + + if (((size_t)ret + written) >= sizeof(command)) { + command[written] = '\0'; + break; + } + + written += ret; + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::getInterfaceList(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "interface list"); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::getConfig(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "interface getcfg %s", GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::setConfig(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + if (SDK_VERSION >= 16) { + snprintf(command, MAX_COMMAND_SIZE - 1, "interface setcfg %s %s %s %s", + GET_CHAR(mIfname), + GET_CHAR(mIp), + GET_CHAR(mPrefix), + GET_CHAR(mLink)); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, "interface setcfg %s %s %s [%s]", + GET_CHAR(mIfname), + GET_CHAR(mIp), + GET_CHAR(mPrefix), + GET_CHAR(mLink)); + } + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::clearAddrForInterface(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "interface clearaddrs %s", GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::createNetwork(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "network create %d", GET_FIELD(mNetId)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::destroyNetwork(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "network destroy %d", GET_FIELD(mNetId)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::addInterfaceToNetwork(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "network interface add %d %s", + GET_FIELD(mNetId), GET_CHAR(mIfname)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::addRouteToInterface(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + struct MyCallback { + static void callback(CommandCallback::CallbackType aOriginalCallback, + CommandChain* aChain, + bool aError, + mozilla::dom::NetworkResultOptions& aResult) + { + NS_ConvertUTF16toUTF8 reason(aResult.mResultReason); + NU_DBG("addRouteToInterface's reason: %s", reason.get()); + if (aError && reason.EqualsASCII("addRoute() failed (File exists)")) { + NU_DBG("Ignore \"File exists\" error when adding host route."); + return aOriginalCallback(aChain, false, aResult); + } + aOriginalCallback(aChain, aError, aResult); + } + }; + + CommandCallback wrappedCallback(MyCallback::callback, aCallback); + modifyRouteOnInterface(aChain, wrappedCallback, aResult, true); +} + +void NetworkUtils::removeRouteFromInterface(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + modifyRouteOnInterface(aChain, aCallback, aResult, false); +} + +void NetworkUtils::modifyRouteOnInterface(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult, + bool aDoAdd) +{ + char command[MAX_COMMAND_SIZE]; + + // AOSP adds host route to its interface table but it doesn't work for + // B2G because we cannot set fwmark per application. So, we add + // all host routes to legacy_system table except scope link route. + + nsCString ipOrSubnetIp = NS_ConvertUTF16toUTF8(GET_FIELD(mIp)); + nsCString gatewayOrEmpty; + const char* legacyOrEmpty = "legacy 0 "; // Add to legacy by default. + if (GET_FIELD(mGateway).IsEmpty()) { + ipOrSubnetIp = getSubnetIp(ipOrSubnetIp, GET_FIELD(mPrefixLength)); + legacyOrEmpty = ""; // Add to interface table for scope link route. + } else { + gatewayOrEmpty = nsCString(" ") + NS_ConvertUTF16toUTF8(GET_FIELD(mGateway)); + } + + const char* action = aDoAdd ? "add" : "remove"; + + snprintf(command, MAX_COMMAND_SIZE - 1, "network route %s%s %d %s %s/%d%s", + legacyOrEmpty, action, + GET_FIELD(mNetId), GET_CHAR(mIfname), ipOrSubnetIp.get(), + GET_FIELD(mPrefixLength), gatewayOrEmpty.get()); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::addDefaultRouteToNetwork(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + if (GET_FIELD(mLoopIndex) >= GET_FIELD(mGateways).Length()) { + aCallback(aChain, false, aResult); + return; + } + + char command[MAX_COMMAND_SIZE]; + nsTArray<nsString>& gateways = GET_FIELD(mGateways); + NS_ConvertUTF16toUTF8 autoGateway(gateways[GET_FIELD(mLoopIndex)]); + + int type = getIpType(autoGateway.get()); + snprintf(command, MAX_COMMAND_SIZE - 1, "network route add %d %s %s/0 %s", + GET_FIELD(mNetId), GET_CHAR(mIfname), + type == AF_INET6 ? "::" : "0.0.0.0", autoGateway.get()); + + struct MyCallback { + static void callback(CommandCallback::CallbackType aOriginalCallback, + CommandChain* aChain, + bool aError, + mozilla::dom::NetworkResultOptions& aResult) + { + NS_ConvertUTF16toUTF8 reason(aResult.mResultReason); + NU_DBG("addDefaultRouteToNetwork's reason: %s", reason.get()); + if (aError && !reason.EqualsASCII("addRoute() failed (File exists)")) { + return aOriginalCallback(aChain, aError, aResult); + } + + GET_FIELD(mLoopIndex)++; + return addDefaultRouteToNetwork(aChain, aOriginalCallback, aResult); + } + }; + + CommandCallback wrappedCallback(MyCallback::callback, aCallback); + doCommand(command, aChain, wrappedCallback); +} + +void NetworkUtils::setDefaultNetwork(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "network default set %d", GET_FIELD(mNetId)); + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::addRouteToSecondaryTable(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) { + + char command[MAX_COMMAND_SIZE]; + + if (SDK_VERSION >= 20) { + snprintf(command, MAX_COMMAND_SIZE - 1, + "network route add %d %s %s/%s %s", + GET_FIELD(mNetId), + GET_CHAR(mIfname), + GET_CHAR(mIp), + GET_CHAR(mPrefix), + GET_CHAR(mGateway)); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, + "interface route add %s secondary %s %s %s", + GET_CHAR(mIfname), + GET_CHAR(mIp), + GET_CHAR(mPrefix), + GET_CHAR(mGateway)); + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::removeRouteFromSecondaryTable(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) { + char command[MAX_COMMAND_SIZE]; + + if (SDK_VERSION >= 20) { + snprintf(command, MAX_COMMAND_SIZE - 1, + "network route remove %d %s %s/%s %s", + GET_FIELD(mNetId), + GET_CHAR(mIfname), + GET_CHAR(mIp), + GET_CHAR(mPrefix), + GET_CHAR(mGateway)); + } else { + snprintf(command, MAX_COMMAND_SIZE - 1, + "interface route remove %s secondary %s %s %s", + GET_CHAR(mIfname), + GET_CHAR(mIp), + GET_CHAR(mPrefix), + GET_CHAR(mGateway)); + } + + doCommand(command, aChain, aCallback); +} + +void NetworkUtils::setIpv6Enabled(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult, + bool aEnabled) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "interface ipv6 %s %s", + GET_CHAR(mIfname), aEnabled ? "enable" : "disable"); + + struct MyCallback { + static void callback(CommandCallback::CallbackType aOriginalCallback, + CommandChain* aChain, + bool aError, + mozilla::dom::NetworkResultOptions& aResult) + { + aOriginalCallback(aChain, false, aResult); + } + }; + + CommandCallback wrappedCallback(MyCallback::callback, aCallback); + doCommand(command, aChain, wrappedCallback); +} + +void NetworkUtils::enableIpv6(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + setIpv6Enabled(aChain, aCallback, aResult, true); +} + +void NetworkUtils::disableIpv6(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + setIpv6Enabled(aChain, aCallback, aResult, false); +} + +void NetworkUtils::setMtu(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char command[MAX_COMMAND_SIZE]; + snprintf(command, MAX_COMMAND_SIZE - 1, "interface setmtu %s %ld", + GET_CHAR(mIfname), GET_FIELD(mMtu)); + + doCommand(command, aChain, aCallback); +} + +#undef GET_CHAR +#undef GET_FIELD + +/* + * Netd command success/fail function + */ +#define ASSIGN_FIELD(prop) aResult.prop = aChain->getParams().prop; +#define ASSIGN_FIELD_VALUE(prop, value) aResult.prop = value; + +template<size_t N> +void NetworkUtils::runChain(const NetworkParams& aParams, + const CommandFunc (&aCmds)[N], + ErrorCallback aError) +{ + CommandChain* chain = new CommandChain(aParams, aCmds, N, aError); + gCommandChainQueue.AppendElement(chain); + + if (gCommandChainQueue.Length() > 1) { + NU_DBG("%d command chains are queued. Wait!", gCommandChainQueue.Length()); + return; + } + + NetworkResultOptions result; + NetworkUtils::next(gCommandChainQueue[0], false, result); +} + +// Called to clean up the command chain and process the queued command chain if any. +void NetworkUtils::finalizeSuccess(CommandChain* aChain, + NetworkResultOptions& aResult) +{ + next(aChain, false, aResult); +} + +void NetworkUtils::wifiTetheringFail(NetworkParams& aOptions, NetworkResultOptions& aResult) +{ + // Notify the main thread. + postMessage(aOptions, aResult); + + // If one of the stages fails, we try roll back to ensure + // we don't leave the network systems in limbo. + ASSIGN_FIELD_VALUE(mEnable, false) + runChain(aOptions, sWifiFailChain, nullptr); +} + +void NetworkUtils::wifiTetheringSuccess(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + ASSIGN_FIELD(mEnable) + + if (aChain->getParams().mEnable) { + MOZ_ASSERT(!gWifiTetheringParms); + gWifiTetheringParms = new NetworkParams(aChain->getParams()); + } + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +void NetworkUtils::usbTetheringFail(NetworkParams& aOptions, + NetworkResultOptions& aResult) +{ + // Notify the main thread. + postMessage(aOptions, aResult); + // Try to roll back to ensure + // we don't leave the network systems in limbo. + // This parameter is used to disable ipforwarding. + { + aOptions.mEnable = false; + runChain(aOptions, sUSBFailChain, nullptr); + } + + // Disable usb rndis function. + { + NetworkParams options; + options.mEnable = false; + options.mReport = false; + gNetworkUtils->enableUsbRndis(options); + } +} + +void NetworkUtils::usbTetheringSuccess(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + ASSIGN_FIELD(mEnable) + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +void NetworkUtils::networkInterfaceAlarmFail(NetworkParams& aOptions, NetworkResultOptions& aResult) +{ + postMessage(aOptions, aResult); +} + +void NetworkUtils::networkInterfaceAlarmSuccess(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + // TODO : error is not used , and it is conflict with boolean type error. + // params.error = parseFloat(params.resultReason); + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +void NetworkUtils::updateUpStreamFail(NetworkParams& aOptions, NetworkResultOptions& aResult) +{ + postMessage(aOptions, aResult); +} + +void NetworkUtils::updateUpStreamSuccess(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + ASSIGN_FIELD(mCurExternalIfname) + ASSIGN_FIELD(mCurInternalIfname) + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +void NetworkUtils::setDhcpServerFail(NetworkParams& aOptions, NetworkResultOptions& aResult) +{ + aResult.mSuccess = false; + postMessage(aOptions, aResult); +} + +void NetworkUtils::setDhcpServerSuccess(CommandChain* aChain, CommandCallback aCallback, NetworkResultOptions& aResult) +{ + aResult.mSuccess = true; + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +void NetworkUtils::wifiOperationModeFail(NetworkParams& aOptions, NetworkResultOptions& aResult) +{ + postMessage(aOptions, aResult); +} + +void NetworkUtils::wifiOperationModeSuccess(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +void NetworkUtils::setDnsFail(NetworkParams& aOptions, NetworkResultOptions& aResult) +{ + postMessage(aOptions, aResult); +} + +void NetworkUtils::defaultAsyncSuccessHandler(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + NU_DBG("defaultAsyncSuccessHandler"); + aResult.mRet = true; + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +void NetworkUtils::defaultAsyncFailureHandler(NetworkParams& aOptions, NetworkResultOptions& aResult) +{ + aResult.mRet = false; + postMessage(aOptions, aResult); +} + +void NetworkUtils::getInterfacesFail(NetworkParams& aOptions, NetworkResultOptions& aResult) +{ + postMessage(aOptions, aResult); +} + +void NetworkUtils::getInterfacesSuccess(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char buf[BUF_SIZE]; + NS_ConvertUTF16toUTF8 reason(aResult.mResultReason); + memcpy(buf, reason.get(), strlen(reason.get())); + + nsTArray<nsCString> result; + split(buf, INTERFACE_DELIMIT, result); + + nsTArray<nsString> interfaceList; + uint32_t length = result.Length(); + convertUTF8toUTF16(result, interfaceList, length); + + aResult.mInterfaceList.Construct(); + for (uint32_t i = 0; i < length; i++) { + aResult.mInterfaceList.Value().AppendElement(interfaceList[i], fallible_t()); + } + + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +void NetworkUtils::getInterfaceConfigFail(NetworkParams& aOptions, + NetworkResultOptions& aResult) +{ + postMessage(aOptions, aResult); +} + +void NetworkUtils::getInterfaceConfigSuccess(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + char buf[BUF_SIZE]; + NS_ConvertUTF16toUTF8 reason(aResult.mResultReason); + memcpy(buf, reason.get(), strlen(reason.get())); + + nsTArray<nsCString> result; + split(buf, NETD_MESSAGE_DELIMIT, result); + + ASSIGN_FIELD_VALUE(mMacAddr, NS_ConvertUTF8toUTF16(result[0])) + ASSIGN_FIELD_VALUE(mIpAddr, NS_ConvertUTF8toUTF16(result[1])) + ASSIGN_FIELD_VALUE(mPrefixLength, atol(result[2].get())) + + if (result[3].Find("up")) { + ASSIGN_FIELD_VALUE(mFlag, NS_ConvertUTF8toUTF16("up")) + } else { + ASSIGN_FIELD_VALUE(mFlag, NS_ConvertUTF8toUTF16("down")) + } + + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +void NetworkUtils::setInterfaceConfigFail(NetworkParams& aOptions, + NetworkResultOptions& aResult) +{ + postMessage(aOptions, aResult); +} + +void NetworkUtils::setInterfaceConfigSuccess(CommandChain* aChain, + CommandCallback aCallback, + NetworkResultOptions& aResult) +{ + postMessage(aChain->getParams(), aResult); + finalizeSuccess(aChain, aResult); +} + +#undef ASSIGN_FIELD +#undef ASSIGN_FIELD_VALUE + +NetworkUtils::NetworkUtils(MessageCallback aCallback) + : mMessageCallback(aCallback) +{ + mNetUtils = new NetUtils(); + + char value[Property::VALUE_MAX_LENGTH]; + Property::Get("ro.build.version.sdk", value, nullptr); + SDK_VERSION = atoi(value); + + Property::Get(IPV6_TETHERING, value, "0"); + SUPPORT_IPV6_TETHERING = atoi(value); + + gNetworkUtils = this; +} + +NetworkUtils::~NetworkUtils() +{ +} + +#define GET_CHAR(prop) NS_ConvertUTF16toUTF8(aOptions.prop).get() +#define GET_FIELD(prop) aOptions.prop + +// Hoist this type definition to global to avoid template +// instantiation error on gcc 4.4 used by ICS emulator. +typedef CommandResult (NetworkUtils::*CommandHandler)(NetworkParams&); +struct CommandHandlerEntry +{ + const char* mCommandName; + CommandHandler mCommandHandler; +}; + +void NetworkUtils::ExecuteCommand(NetworkParams aOptions) +{ + const static CommandHandlerEntry + COMMAND_HANDLER_TABLE[] = { + + // For command 'testCommand', BUILD_ENTRY(testCommand) will generate + // {"testCommand", NetworkUtils::testCommand} + #define BUILD_ENTRY(c) {#c, &NetworkUtils::c} + + BUILD_ENTRY(removeNetworkRoute), + BUILD_ENTRY(setDNS), + BUILD_ENTRY(setDefaultRoute), + BUILD_ENTRY(removeDefaultRoute), + BUILD_ENTRY(addHostRoute), + BUILD_ENTRY(removeHostRoute), + BUILD_ENTRY(addSecondaryRoute), + BUILD_ENTRY(removeSecondaryRoute), + BUILD_ENTRY(setNetworkInterfaceAlarm), + BUILD_ENTRY(enableNetworkInterfaceAlarm), + BUILD_ENTRY(disableNetworkInterfaceAlarm), + BUILD_ENTRY(setTetheringAlarm), + BUILD_ENTRY(removeTetheringAlarm), + BUILD_ENTRY(getTetheringStatus), + BUILD_ENTRY(setWifiOperationMode), + BUILD_ENTRY(setDhcpServer), + BUILD_ENTRY(setWifiTethering), + BUILD_ENTRY(setUSBTethering), + BUILD_ENTRY(enableUsbRndis), + BUILD_ENTRY(updateUpStream), + BUILD_ENTRY(configureInterface), + BUILD_ENTRY(dhcpRequest), + BUILD_ENTRY(stopDhcp), + BUILD_ENTRY(enableInterface), + BUILD_ENTRY(disableInterface), + BUILD_ENTRY(resetConnections), + BUILD_ENTRY(createNetwork), + BUILD_ENTRY(destroyNetwork), + BUILD_ENTRY(getNetId), + BUILD_ENTRY(getInterfaces), + BUILD_ENTRY(getInterfaceConfig), + BUILD_ENTRY(setInterfaceConfig), + BUILD_ENTRY(setMtu), + + #undef BUILD_ENTRY + }; + + // Loop until we find the command name which matches aOptions.mCmd. + CommandHandler handler = nullptr; + for (size_t i = 0; i < mozilla::ArrayLength(COMMAND_HANDLER_TABLE); i++) { + if (aOptions.mCmd.EqualsASCII(COMMAND_HANDLER_TABLE[i].mCommandName)) { + handler = COMMAND_HANDLER_TABLE[i].mCommandHandler; + break; + } + } + + if (!handler) { + // Command not found in COMMAND_HANDLER_TABLE. + WARN("unknown message: %s", NS_ConvertUTF16toUTF8(aOptions.mCmd).get()); + return; + } + + // The handler would return one of the following 3 values + // to be wrapped to CommandResult: + // + // 1) |int32_t| for mostly synchronous native function calls. + // 2) |NetworkResultOptions| to populate additional results. (e.g. dhcpRequest) + // 3) |CommandResult::Pending| to indicate the result is not + // obtained yet. + // + // If the handler returns "Pending", the handler should take the + // responsibility for posting result to main thread. + CommandResult commandResult = (this->*handler)(aOptions); + if (!commandResult.isPending()) { + postMessage(aOptions, commandResult.mResult); + } +} + +/** + * Handle received data from netd. + */ +void NetworkUtils::onNetdMessage(NetdCommand* aCommand) +{ + char* data = (char*)aCommand->mData; + + // get code & reason. + char* result = strtok(data, NETD_MESSAGE_DELIMIT); + + if (!result) { + nextNetdCommand(); + return; + } + uint32_t code = atoi(result); + + if (!isBroadcastMessage(code) && SDK_VERSION >= 16) { + strtok(nullptr, NETD_MESSAGE_DELIMIT); + } + + char* reason = strtok(nullptr, "\0"); + + if (isBroadcastMessage(code)) { + NU_DBG("Receiving broadcast message from netd."); + NU_DBG(" ==> Code: %d Reason: %s", code, reason); + sendBroadcastMessage(code, reason); + + if (code == NETD_COMMAND_INTERFACE_CHANGE) { + if (gWifiTetheringParms) { + char linkdownReason[MAX_COMMAND_SIZE]; + snprintf(linkdownReason, MAX_COMMAND_SIZE - 1, + "Iface linkstate %s down", + NS_ConvertUTF16toUTF8(gWifiTetheringParms->mIfname).get()); + + if (!strcmp(reason, linkdownReason)) { + NU_DBG("Wifi link down, restarting tethering."); + runChain(*gWifiTetheringParms, sWifiRetryChain, wifiTetheringFail); + } + } + } + + nextNetdCommand(); + return; + } + + // Set pending to false before we handle next command. + NU_DBG("Receiving \"%s\" command response from netd.", gCurrentCommand.command); + NU_DBG(" ==> Code: %d Reason: %s", code, reason); + + gReason.AppendElement(nsCString(reason)); + + // 1xx response code regards as command is proceeding, we need to wait for + // final response code such as 2xx, 4xx and 5xx before sending next command. + if (isProceeding(code)) { + return; + } + + if (isComplete(code)) { + gPending = false; + } + + { + char buf[BUF_SIZE]; + join(gReason, INTERFACE_DELIMIT, BUF_SIZE, buf); + + NetworkResultOptions result; + result.mResultCode = code; + result.mResultReason = NS_ConvertUTF8toUTF16(buf); + (gCurrentCommand.callback)(gCurrentCommand.chain, isError(code), result); + gReason.Clear(); + } + + // Handling pending commands if any. + if (isComplete(code)) { + nextNetdCommand(); + } +} + +/** + * Start/Stop DHCP server. + */ +CommandResult NetworkUtils::setDhcpServer(NetworkParams& aOptions) +{ + if (aOptions.mEnabled) { + aOptions.mWifiStartIp = aOptions.mStartIp; + aOptions.mWifiEndIp = aOptions.mEndIp; + aOptions.mIp = aOptions.mServerIp; + aOptions.mPrefix = aOptions.mMaskLength; + aOptions.mLink = NS_ConvertUTF8toUTF16("up"); + + runChain(aOptions, sStartDhcpServerChain, setDhcpServerFail); + } else { + runChain(aOptions, sStopDhcpServerChain, setDhcpServerFail); + } + return CommandResult::Pending(); +} + +/** + * Set DNS servers for given network interface. + */ +CommandResult NetworkUtils::setDNS(NetworkParams& aOptions) +{ + uint32_t length = aOptions.mDnses.Length(); + + if (length > 0) { + for (uint32_t i = 0; i < length; i++) { + NS_ConvertUTF16toUTF8 autoDns(aOptions.mDnses[i]); + + char dns_prop_key[Property::VALUE_MAX_LENGTH]; + SprintfLiteral(dns_prop_key, "net.dns%d", i+1); + Property::Set(dns_prop_key, autoDns.get()); + } + } else { + // Set dnses from system properties. + IFProperties interfaceProperties; + getIFProperties(GET_CHAR(mIfname), interfaceProperties); + + Property::Set("net.dns1", interfaceProperties.dns1); + Property::Set("net.dns2", interfaceProperties.dns2); + } + + // Bump the DNS change property. + char dnschange[Property::VALUE_MAX_LENGTH]; + Property::Get("net.dnschange", dnschange, "0"); + + char num[Property::VALUE_MAX_LENGTH]; + snprintf(num, Property::VALUE_MAX_LENGTH - 1, "%d", atoi(dnschange) + 1); + Property::Set("net.dnschange", num); + + // DNS needs to be set through netd since JellyBean (4.3). + if (SDK_VERSION >= 20) { + // Lollipop. + static CommandFunc COMMAND_CHAIN[] = { + setInterfaceDns, + addDefaultRouteToNetwork, + defaultAsyncSuccessHandler + }; + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(aOptions.mIfname, &netIdInfo)) { + return -1; + } + aOptions.mNetId = netIdInfo.mNetId; + runChain(aOptions, COMMAND_CHAIN, setDnsFail); + return CommandResult::Pending(); + } + if (SDK_VERSION >= 18) { + // JB, KK. + static CommandFunc COMMAND_CHAIN[] = { + #if ANDROID_VERSION == 18 + // Since we don't use per-interface DNS lookup feature on JB, + // we need to set the default DNS interface whenever setting the + // DNS name server. + setDefaultInterface, + #endif + setInterfaceDns, + defaultAsyncSuccessHandler + }; + runChain(aOptions, COMMAND_CHAIN, setDnsFail); + return CommandResult::Pending(); + } + + return SUCCESS; +} + +CommandResult NetworkUtils::configureInterface(NetworkParams& aOptions) +{ + NS_ConvertUTF16toUTF8 autoIfname(aOptions.mIfname); + return mNetUtils->do_ifc_configure( + autoIfname.get(), + aOptions.mIpaddr, + aOptions.mMask, + aOptions.mGateway_long, + aOptions.mDns1_long, + aOptions.mDns2_long + ); +} + +CommandResult NetworkUtils::stopDhcp(NetworkParams& aOptions) +{ + return mNetUtils->do_dhcp_stop(GET_CHAR(mIfname)); +} + +CommandResult NetworkUtils::dhcpRequest(NetworkParams& aOptions) { + mozilla::dom::NetworkResultOptions result; + + NS_ConvertUTF16toUTF8 autoIfname(aOptions.mIfname); + char ipaddr[Property::VALUE_MAX_LENGTH]; + char gateway[Property::VALUE_MAX_LENGTH]; + uint32_t prefixLength; + char dns1[Property::VALUE_MAX_LENGTH]; + char dns2[Property::VALUE_MAX_LENGTH]; + char server[Property::VALUE_MAX_LENGTH]; + uint32_t lease; + char vendorinfo[Property::VALUE_MAX_LENGTH]; + int32_t ret = mNetUtils->do_dhcp_do_request(autoIfname.get(), + ipaddr, + gateway, + &prefixLength, + dns1, + dns2, + server, + &lease, + vendorinfo); + + RETURN_IF_FAILED(ret); + + result.mIpaddr_str = NS_ConvertUTF8toUTF16(ipaddr); + result.mGateway_str = NS_ConvertUTF8toUTF16(gateway); + result.mDns1_str = NS_ConvertUTF8toUTF16(dns1); + result.mDns2_str = NS_ConvertUTF8toUTF16(dns2); + result.mServer_str = NS_ConvertUTF8toUTF16(server); + result.mVendor_str = NS_ConvertUTF8toUTF16(vendorinfo); + result.mLease = lease; + result.mPrefixLength = prefixLength; + result.mMask = makeMask(prefixLength); + + uint32_t inet4; // only support IPv4 for now. + +#define INET_PTON(var, field) \ + PR_BEGIN_MACRO \ + inet_pton(AF_INET, var, &inet4); \ + result.field = inet4; \ + PR_END_MACRO + + INET_PTON(ipaddr, mIpaddr); + INET_PTON(gateway, mGateway); + + if (dns1[0] != '\0') { + INET_PTON(dns1, mDns1); + } + + if (dns2[0] != '\0') { + INET_PTON(dns2, mDns2); + } + + INET_PTON(server, mServer); + + char inet_str[64]; + if (inet_ntop(AF_INET, &result.mMask, inet_str, sizeof(inet_str))) { + result.mMask_str = NS_ConvertUTF8toUTF16(inet_str); + } + + return result; +} + +CommandResult NetworkUtils::enableInterface(NetworkParams& aOptions) { + return mNetUtils->do_ifc_enable( + NS_ConvertUTF16toUTF8(aOptions.mIfname).get()); +} + +CommandResult NetworkUtils::disableInterface(NetworkParams& aOptions) { + return mNetUtils->do_ifc_disable( + NS_ConvertUTF16toUTF8(aOptions.mIfname).get()); +} + +CommandResult NetworkUtils::resetConnections(NetworkParams& aOptions) { + NS_ConvertUTF16toUTF8 autoIfname(aOptions.mIfname); + return mNetUtils->do_ifc_reset_connections( + NS_ConvertUTF16toUTF8(aOptions.mIfname).get(), + RESET_ALL_ADDRESSES); +} + +/** + * Set default route and DNS servers for given network interface. + */ +CommandResult NetworkUtils::setDefaultRoute(NetworkParams& aOptions) +{ + if (SDK_VERSION < 20) { + return setDefaultRouteLegacy(aOptions); + } + + static CommandFunc COMMAND_CHAIN[] = { + addDefaultRouteToNetwork, + setDefaultNetwork, + defaultAsyncSuccessHandler, + }; + + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(GET_FIELD(mIfname), &netIdInfo)) { + ERROR("No such interface"); + return -1; + } + + aOptions.mNetId = netIdInfo.mNetId; + aOptions.mLoopIndex = 0; + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + + return CommandResult::Pending(); +} + +/** + * Set default route and DNS servers for given network interface by obsoleted libnetutils. + */ +CommandResult NetworkUtils::setDefaultRouteLegacy(NetworkParams& aOptions) +{ + NS_ConvertUTF16toUTF8 autoIfname(aOptions.mIfname); + + uint32_t length = aOptions.mGateways.Length(); + if (length > 0) { + for (uint32_t i = 0; i < length; i++) { + NS_ConvertUTF16toUTF8 autoGateway(aOptions.mGateways[i]); + + int type = getIpType(autoGateway.get()); + if (type != AF_INET && type != AF_INET6) { + continue; + } + + if (type == AF_INET6) { + RETURN_IF_FAILED(mNetUtils->do_ifc_add_route(autoIfname.get(), "::", 0, autoGateway.get())); + } else { /* type == AF_INET */ + RETURN_IF_FAILED(mNetUtils->do_ifc_set_default_route(autoIfname.get(), inet_addr(autoGateway.get()))); + } + } + } else { + // Set default froute from system properties. + char key[Property::KEY_MAX_LENGTH]; + char gateway[Property::KEY_MAX_LENGTH]; + + snprintf(key, sizeof key - 1, "net.%s.gw", autoIfname.get()); + Property::Get(key, gateway, ""); + + int type = getIpType(gateway); + if (type != AF_INET && type != AF_INET6) { + return EAFNOSUPPORT; + } + + if (type == AF_INET6) { + RETURN_IF_FAILED(mNetUtils->do_ifc_add_route(autoIfname.get(), "::", 0, gateway)); + } else { /* type == AF_INET */ + RETURN_IF_FAILED(mNetUtils->do_ifc_set_default_route(autoIfname.get(), inet_addr(gateway))); + } + } + + // Set the default DNS interface. + if (SDK_VERSION >= 18) { + // For JB, KK only. + static CommandFunc COMMAND_CHAIN[] = { + setDefaultInterface, + defaultAsyncSuccessHandler + }; + runChain(aOptions, COMMAND_CHAIN, setDnsFail); + return CommandResult::Pending(); + } + + return SUCCESS; +} + +/** + * Remove default route for given network interface. + */ +CommandResult NetworkUtils::removeDefaultRoute(NetworkParams& aOptions) +{ + NU_DBG("Calling NetworkUtils::removeDefaultRoute"); + + if (SDK_VERSION < 20) { + return removeDefaultRouteLegacy(aOptions); + } + + static CommandFunc COMMAND_CHAIN[] = { + removeDefaultRoute, + defaultAsyncSuccessHandler, + }; + + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(GET_FIELD(mIfname), &netIdInfo)) { + ERROR("No such interface: %s", GET_CHAR(mIfname)); + return -1; + } + + NU_DBG("Obtained netid %d for interface %s", netIdInfo.mNetId, GET_CHAR(mIfname)); + + aOptions.mNetId = netIdInfo.mNetId; + aOptions.mLoopIndex = 0; + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + + return CommandResult::Pending(); +} + +/** + * Remove default route for given network interface by obsoleted libnetutils. + */ +CommandResult NetworkUtils::removeDefaultRouteLegacy(NetworkParams& aOptions) +{ + // Legacy libnetutils calls before Lollipop. + uint32_t length = aOptions.mGateways.Length(); + for (uint32_t i = 0; i < length; i++) { + NS_ConvertUTF16toUTF8 autoGateway(aOptions.mGateways[i]); + + int type = getIpType(autoGateway.get()); + if (type != AF_INET && type != AF_INET6) { + return EAFNOSUPPORT; + } + + WARN_IF_FAILED(mNetUtils->do_ifc_remove_route(GET_CHAR(mIfname), + type == AF_INET ? "0.0.0.0" : "::", + 0, autoGateway.get())); + } + + return SUCCESS; +} + +/** + * Add host route for given network interface. + */ +CommandResult NetworkUtils::addHostRoute(NetworkParams& aOptions) +{ + if (SDK_VERSION < 20) { + return addHostRouteLegacy(aOptions); + } + + static CommandFunc COMMAND_CHAIN[] = { + addRouteToInterface, + defaultAsyncSuccessHandler, + }; + + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(GET_FIELD(mIfname), &netIdInfo)) { + ERROR("No such interface: %s", GET_CHAR(mIfname)); + return -1; + } + + NU_DBG("Obtained netid %d for interface %s", netIdInfo.mNetId, GET_CHAR(mIfname)); + + aOptions.mNetId = netIdInfo.mNetId; + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + + return CommandResult::Pending(); +} + +/** + * Add host route for given network interface. + */ +CommandResult NetworkUtils::addHostRouteLegacy(NetworkParams& aOptions) +{ + if (aOptions.mGateway.IsEmpty()) { + ERROR("addHostRouteLegacy does not support empty gateway."); + return EINVAL; + } + + NS_ConvertUTF16toUTF8 autoIfname(aOptions.mIfname); + NS_ConvertUTF16toUTF8 autoHostname(aOptions.mIp); + NS_ConvertUTF16toUTF8 autoGateway(aOptions.mGateway); + int type, prefix; + + type = getIpType(autoHostname.get()); + if (type != AF_INET && type != AF_INET6) { + return EAFNOSUPPORT; + } + + if (type != getIpType(autoGateway.get())) { + return EINVAL; + } + + prefix = type == AF_INET ? 32 : 128; + return mNetUtils->do_ifc_add_route(autoIfname.get(), autoHostname.get(), + prefix, autoGateway.get()); +} + +/** + * Remove host route for given network interface. + */ +CommandResult NetworkUtils::removeHostRoute(NetworkParams& aOptions) +{ + if (SDK_VERSION < 20) { + return removeHostRouteLegacy(aOptions); + } + + static CommandFunc COMMAND_CHAIN[] = { + removeRouteFromInterface, + defaultAsyncSuccessHandler, + }; + + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(GET_FIELD(mIfname), &netIdInfo)) { + ERROR("No such interface: %s", GET_CHAR(mIfname)); + return -1; + } + + NU_DBG("Obtained netid %d for interface %s", netIdInfo.mNetId, GET_CHAR(mIfname)); + + aOptions.mNetId = netIdInfo.mNetId; + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + + return CommandResult::Pending(); +} + +/** + * Remove host route for given network interface. + */ +CommandResult NetworkUtils::removeHostRouteLegacy(NetworkParams& aOptions) +{ + NS_ConvertUTF16toUTF8 autoIfname(aOptions.mIfname); + NS_ConvertUTF16toUTF8 autoHostname(aOptions.mIp); + NS_ConvertUTF16toUTF8 autoGateway(aOptions.mGateway); + int type, prefix; + + type = getIpType(autoHostname.get()); + if (type != AF_INET && type != AF_INET6) { + return EAFNOSUPPORT; + } + + if (type != getIpType(autoGateway.get())) { + return EINVAL; + } + + prefix = type == AF_INET ? 32 : 128; + return mNetUtils->do_ifc_remove_route(autoIfname.get(), autoHostname.get(), + prefix, autoGateway.get()); +} + +CommandResult NetworkUtils::removeNetworkRoute(NetworkParams& aOptions) +{ + if (SDK_VERSION < 20) { + return removeNetworkRouteLegacy(aOptions); + } + + static CommandFunc COMMAND_CHAIN[] = { + clearAddrForInterface, + defaultAsyncSuccessHandler, + }; + + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(GET_FIELD(mIfname), &netIdInfo)) { + ERROR("interface %s is not present in any network", GET_CHAR(mIfname)); + return -1; + } + + NU_DBG("Obtained netid %d for interface %s", netIdInfo.mNetId, GET_CHAR(mIfname)); + + aOptions.mNetId = netIdInfo.mNetId; + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + + return CommandResult::Pending(); +} + +nsCString NetworkUtils::getSubnetIp(const nsCString& aIp, int aPrefixLength) +{ + int type = getIpType(aIp.get()); + + if (AF_INET6 == type) { + struct in6_addr in6; + if (inet_pton(AF_INET6, aIp.get(), &in6) != 1) { + return nsCString(); + } + + uint32_t p, i, p1, mask; + p = aPrefixLength; + i = 0; + while (i < 4) { + p1 = p > 32 ? 32 : p; + p -= p1; + mask = p1 ? ~0x0 << (32 - p1) : 0; + in6.s6_addr32[i++] &= htonl(mask); + } + + char subnetStr[INET6_ADDRSTRLEN]; + if (!inet_ntop(AF_INET6, &in6, subnetStr, sizeof subnetStr)) { + return nsCString(); + } + + return nsCString(subnetStr); + } + + if (AF_INET == type) { + uint32_t ip = inet_addr(aIp.get()); + uint32_t netmask = makeMask(aPrefixLength); + uint32_t subnet = ip & netmask; + struct in_addr addr; + addr.s_addr = subnet; + return nsCString(inet_ntoa(addr)); + } + + return nsCString(); +} + +CommandResult NetworkUtils::removeNetworkRouteLegacy(NetworkParams& aOptions) +{ + NS_ConvertUTF16toUTF8 autoIfname(aOptions.mIfname); + NS_ConvertUTF16toUTF8 autoIp(aOptions.mIp); + + int type = getIpType(autoIp.get()); + if (type != AF_INET && type != AF_INET6) { + return EAFNOSUPPORT; + } + + uint32_t prefixLength = GET_FIELD(mPrefixLength); + + if (type == AF_INET6) { + // Calculate subnet. + struct in6_addr in6; + if (inet_pton(AF_INET6, autoIp.get(), &in6) != 1) { + return EINVAL; + } + + uint32_t p, i, p1, mask; + p = prefixLength; + i = 0; + while (i < 4) { + p1 = p > 32 ? 32 : p; + p -= p1; + mask = p1 ? ~0x0 << (32 - p1) : 0; + in6.s6_addr32[i++] &= htonl(mask); + } + + char subnetStr[INET6_ADDRSTRLEN]; + if (!inet_ntop(AF_INET6, &in6, subnetStr, sizeof subnetStr)) { + return EINVAL; + } + + // Remove default route. + WARN_IF_FAILED(mNetUtils->do_ifc_remove_route(autoIfname.get(), "::", 0, NULL)); + + // Remove subnet route. + RETURN_IF_FAILED(mNetUtils->do_ifc_remove_route(autoIfname.get(), subnetStr, prefixLength, NULL)); + return SUCCESS; + } + + /* type == AF_INET */ + uint32_t ip = inet_addr(autoIp.get()); + uint32_t netmask = makeMask(prefixLength); + uint32_t subnet = ip & netmask; + const char* gateway = "0.0.0.0"; + struct in_addr addr; + addr.s_addr = subnet; + const char* dst = inet_ntoa(addr); + + RETURN_IF_FAILED(mNetUtils->do_ifc_remove_default_route(autoIfname.get())); + RETURN_IF_FAILED(mNetUtils->do_ifc_remove_route(autoIfname.get(), dst, prefixLength, gateway)); + return SUCCESS; +} + +CommandResult NetworkUtils::addSecondaryRoute(NetworkParams& aOptions) +{ + static CommandFunc COMMAND_CHAIN[] = { + addRouteToSecondaryTable, + defaultAsyncSuccessHandler + }; + + if (SDK_VERSION >= 20) { + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(aOptions.mIfname, &netIdInfo)) { + return -1; + } + aOptions.mNetId = netIdInfo.mNetId; + } + + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + return CommandResult::Pending(); +} + +CommandResult NetworkUtils::removeSecondaryRoute(NetworkParams& aOptions) +{ + static CommandFunc COMMAND_CHAIN[] = { + removeRouteFromSecondaryTable, + defaultAsyncSuccessHandler + }; + + if (SDK_VERSION >= 20) { + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(aOptions.mIfname, &netIdInfo)) { + return -1; + } + aOptions.mNetId = netIdInfo.mNetId; + } + + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + return CommandResult::Pending(); +} + +CommandResult NetworkUtils::setNetworkInterfaceAlarm(NetworkParams& aOptions) +{ + NU_DBG("setNetworkInterfaceAlarms: %s", GET_CHAR(mIfname)); + runChain(aOptions, sNetworkInterfaceSetAlarmChain, networkInterfaceAlarmFail); + return CommandResult::Pending(); +} + +CommandResult NetworkUtils::enableNetworkInterfaceAlarm(NetworkParams& aOptions) +{ + NU_DBG("enableNetworkInterfaceAlarm: %s", GET_CHAR(mIfname)); + runChain(aOptions, sNetworkInterfaceEnableAlarmChain, networkInterfaceAlarmFail); + return CommandResult::Pending(); +} + +CommandResult NetworkUtils::disableNetworkInterfaceAlarm(NetworkParams& aOptions) +{ + NU_DBG("disableNetworkInterfaceAlarms: %s", GET_CHAR(mIfname)); + runChain(aOptions, sNetworkInterfaceDisableAlarmChain, networkInterfaceAlarmFail); + return CommandResult::Pending(); +} + +CommandResult NetworkUtils::setTetheringAlarm(NetworkParams& aOptions) +{ + NU_DBG("setTetheringAlarm"); + runChain(aOptions, sTetheringInterfaceSetAlarmChain, networkInterfaceAlarmFail); + return CommandResult::Pending(); +} + +CommandResult NetworkUtils::removeTetheringAlarm(NetworkParams& aOptions) +{ + NU_DBG("removeTetheringAlarm"); + runChain(aOptions, sTetheringInterfaceRemoveAlarmChain, networkInterfaceAlarmFail); + return CommandResult::Pending(); +} + +CommandResult NetworkUtils::getTetheringStatus(NetworkParams& aOptions) +{ + NU_DBG("getTetheringStatus"); + runChain(aOptions, sTetheringGetStatusChain, networkInterfaceAlarmFail); + return CommandResult::Pending(); +} + +/** + * handling main thread's reload Wifi firmware request + */ +CommandResult NetworkUtils::setWifiOperationMode(NetworkParams& aOptions) +{ + NU_DBG("setWifiOperationMode: %s %s", GET_CHAR(mIfname), GET_CHAR(mMode)); + runChain(aOptions, sWifiOperationModeChain, wifiOperationModeFail); + return CommandResult::Pending(); +} + +/** + * handling main thread's enable/disable WiFi Tethering request + */ +CommandResult NetworkUtils::setWifiTethering(NetworkParams& aOptions) +{ + bool enable = aOptions.mEnable; + IFProperties interfaceProperties; + getIFProperties(GET_CHAR(mExternalIfname), interfaceProperties); + + if (strcmp(interfaceProperties.dns1, "")) { + int type = getIpType(interfaceProperties.dns1); + if (type != AF_INET6) { + aOptions.mDns1 = NS_ConvertUTF8toUTF16(interfaceProperties.dns1); + } + } + if (strcmp(interfaceProperties.dns2, "")) { + int type = getIpType(interfaceProperties.dns2); + if (type != AF_INET6) { + aOptions.mDns2 = NS_ConvertUTF8toUTF16(interfaceProperties.dns2); + } + } + dumpParams(aOptions, "WIFI"); + + if (SDK_VERSION >= 20) { + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(aOptions.mExternalIfname, &netIdInfo)) { + ERROR("No such interface: %s", GET_CHAR(mExternalIfname)); + return -1; + } + aOptions.mNetId = netIdInfo.mNetId; + } + + if (enable) { + NU_DBG("Starting Wifi Tethering on %s <-> %s", + GET_CHAR(mInternalIfname), GET_CHAR(mExternalIfname)); + runChain(aOptions, sWifiEnableChain, wifiTetheringFail); + } else { + NU_DBG("Stopping Wifi Tethering on %s <-> %s", + GET_CHAR(mInternalIfname), GET_CHAR(mExternalIfname)); + runChain(aOptions, sWifiDisableChain, wifiTetheringFail); + } + return CommandResult::Pending(); +} + +CommandResult NetworkUtils::setUSBTethering(NetworkParams& aOptions) +{ + bool enable = aOptions.mEnable; + IFProperties interfaceProperties; + getIFProperties(GET_CHAR(mExternalIfname), interfaceProperties); + + if (strcmp(interfaceProperties.dns1, "")) { + int type = getIpType(interfaceProperties.dns1); + if (type != AF_INET6) { + aOptions.mDns1 = NS_ConvertUTF8toUTF16(interfaceProperties.dns1); + } + } + if (strcmp(interfaceProperties.dns2, "")) { + int type = getIpType(interfaceProperties.dns2); + if (type != AF_INET6) { + aOptions.mDns2 = NS_ConvertUTF8toUTF16(interfaceProperties.dns2); + } + } + dumpParams(aOptions, "USB"); + + if (SDK_VERSION >= 20) { + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.lookup(aOptions.mExternalIfname, &netIdInfo)) { + ERROR("No such interface: %s", GET_CHAR(mExternalIfname)); + return -1; + } + aOptions.mNetId = netIdInfo.mNetId; + } + + if (enable) { + NU_DBG("Starting USB Tethering on %s <-> %s", + GET_CHAR(mInternalIfname), GET_CHAR(mExternalIfname)); + runChain(aOptions, sUSBEnableChain, usbTetheringFail); + } else { + NU_DBG("Stopping USB Tethering on %s <-> %s", + GET_CHAR(mInternalIfname), GET_CHAR(mExternalIfname)); + runChain(aOptions, sUSBDisableChain, usbTetheringFail); + } + return CommandResult::Pending(); +} + +void NetworkUtils::escapeQuote(nsCString& aString) +{ + aString.ReplaceSubstring("\\", "\\\\"); + aString.ReplaceSubstring("\"", "\\\""); +} + +CommandResult NetworkUtils::checkUsbRndisState(NetworkParams& aOptions) +{ + static uint32_t retry = 0; + + char currentState[Property::VALUE_MAX_LENGTH]; + Property::Get(SYS_USB_STATE_PROPERTY, currentState, nullptr); + + nsTArray<nsCString> stateFuncs; + split(currentState, USB_CONFIG_DELIMIT, stateFuncs); + bool rndisPresent = stateFuncs.Contains(nsCString(USB_FUNCTION_RNDIS)); + + if (aOptions.mEnable == rndisPresent) { + NetworkResultOptions result; + result.mEnable = aOptions.mEnable; + result.mResult = true; + retry = 0; + return result; + } + if (retry < USB_FUNCTION_RETRY_TIMES) { + retry++; + usleep(USB_FUNCTION_RETRY_INTERVAL * 1000); + return checkUsbRndisState(aOptions); + } + + NetworkResultOptions result; + result.mResult = false; + retry = 0; + return result; +} + +/** + * Modify usb function's property to turn on USB RNDIS function + */ +CommandResult NetworkUtils::enableUsbRndis(NetworkParams& aOptions) +{ + bool report = aOptions.mReport; + + // For some reason, rndis doesn't play well with diag,modem,nmea. + // So when turning rndis on, we set sys.usb.config to either "rndis" + // or "rndis,adb". When turning rndis off, we go back to + // persist.sys.usb.config. + // + // On the otoro/unagi, persist.sys.usb.config should be one of: + // + // diag,modem,nmea,mass_storage + // diag,modem,nmea,mass_storage,adb + // + // When rndis is enabled, sys.usb.config should be one of: + // + // rdnis + // rndis,adb + // + // and when rndis is disabled, it should revert to persist.sys.usb.config + + char currentConfig[Property::VALUE_MAX_LENGTH]; + Property::Get(SYS_USB_CONFIG_PROPERTY, currentConfig, nullptr); + + nsTArray<nsCString> configFuncs; + split(currentConfig, USB_CONFIG_DELIMIT, configFuncs); + + char persistConfig[Property::VALUE_MAX_LENGTH]; + Property::Get(PERSIST_SYS_USB_CONFIG_PROPERTY, persistConfig, nullptr); + + nsTArray<nsCString> persistFuncs; + split(persistConfig, USB_CONFIG_DELIMIT, persistFuncs); + + if (aOptions.mEnable) { + configFuncs.Clear(); + configFuncs.AppendElement(nsCString(USB_FUNCTION_RNDIS)); + if (persistFuncs.Contains(nsCString(USB_FUNCTION_ADB))) { + configFuncs.AppendElement(nsCString(USB_FUNCTION_ADB)); + } + } else { + // We're turning rndis off, revert back to the persist setting. + // adb will already be correct there, so we don't need to do any + // further adjustments. + configFuncs = persistFuncs; + } + + char newConfig[Property::VALUE_MAX_LENGTH] = ""; + Property::Get(SYS_USB_CONFIG_PROPERTY, currentConfig, nullptr); + join(configFuncs, USB_CONFIG_DELIMIT, Property::VALUE_MAX_LENGTH, newConfig); + if (strcmp(currentConfig, newConfig)) { + Property::Set(SYS_USB_CONFIG_PROPERTY, newConfig); + } + + // Trigger the timer to check usb state and report the result to NetworkManager. + if (report) { + usleep(USB_FUNCTION_RETRY_INTERVAL * 1000); + return checkUsbRndisState(aOptions); + } + return SUCCESS; +} + +/** + * handling upstream interface change event. + */ +CommandResult NetworkUtils::updateUpStream(NetworkParams& aOptions) +{ + runChain(aOptions, sUpdateUpStreamChain, updateUpStreamFail); + return CommandResult::Pending(); +} + +/** + * handling upstream interface change event. + */ +CommandResult NetworkUtils::createNetwork(NetworkParams& aOptions) +{ + if (SDK_VERSION < 20) { + return SUCCESS; + } + + static CommandFunc COMMAND_CHAIN[] = { + createNetwork, + enableIpv6, + addInterfaceToNetwork, + defaultAsyncSuccessHandler, + }; + + NetIdManager::NetIdInfo netIdInfo; + mNetIdManager.acquire(GET_FIELD(mIfname), &netIdInfo); + if (netIdInfo.mRefCnt > 1) { + // Already created. Just return. + NU_DBG("Interface %s (%d) has been created.", GET_CHAR(mIfname), + netIdInfo.mNetId); + return SUCCESS; + } + + NU_DBG("Request netd to create a network with netid %d", netIdInfo.mNetId); + // Newly created netid. Ask netd to create network. + aOptions.mNetId = netIdInfo.mNetId; + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + + return CommandResult::Pending(); +} + +/** + * handling upstream interface change event. + */ +CommandResult NetworkUtils::destroyNetwork(NetworkParams& aOptions) +{ + if (SDK_VERSION < 20) { + return SUCCESS; + } + + static CommandFunc COMMAND_CHAIN[] = { + disableIpv6, + destroyNetwork, + defaultAsyncSuccessHandler, + }; + + NetIdManager::NetIdInfo netIdInfo; + if (!mNetIdManager.release(GET_FIELD(mIfname), &netIdInfo)) { + ERROR("No existing netid for %s", GET_CHAR(mIfname)); + return -1; + } + + if (netIdInfo.mRefCnt > 0) { + // Still be referenced. Just return. + NU_DBG("Someone is still using this interface."); + return SUCCESS; + } + + NU_DBG("Interface %s (%d) is no longer used. Tell netd to destroy.", + GET_CHAR(mIfname), netIdInfo.mNetId); + + aOptions.mNetId = netIdInfo.mNetId; + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + return CommandResult::Pending(); +} + +/** + * Query the netId associated with the given network interface name. + */ +CommandResult NetworkUtils::getNetId(NetworkParams& aOptions) +{ + NetworkResultOptions result; + + if (SDK_VERSION < 20) { + // For pre-Lollipop, use the interface name as the fallback. + result.mNetId = GET_FIELD(mIfname); + return result; + } + + NetIdManager::NetIdInfo netIdInfo; + if (-1 == mNetIdManager.lookup(GET_FIELD(mIfname), &netIdInfo)) { + return ESRCH; + } + result.mNetId.AppendInt(netIdInfo.mNetId, 10); + return result; +} + +/** + * Get existing network interfaces. + */ +CommandResult NetworkUtils::getInterfaces(NetworkParams& aOptions) +{ + runChain(aOptions, sGetInterfacesChain, getInterfacesFail); + return CommandResult::Pending(); +} + +/** + * Get network config of a specified interface. + */ +CommandResult NetworkUtils::getInterfaceConfig(NetworkParams& aOptions) +{ + runChain(aOptions, sGetInterfaceConfigChain, getInterfaceConfigFail); + return CommandResult::Pending(); +} + +/** + * Set network config for a specified interface. + */ +CommandResult NetworkUtils::setInterfaceConfig(NetworkParams& aOptions) +{ + runChain(aOptions, sSetInterfaceConfigChain, setInterfaceConfigFail); + return CommandResult::Pending(); +} + +CommandResult NetworkUtils::setMtu(NetworkParams& aOptions) +{ + // Setting/getting mtu is supported since Kitkat. + if (SDK_VERSION < 19) { + ERROR("setMtu is not supported in current SDK_VERSION."); + return -1; + } + + static CommandFunc COMMAND_CHAIN[] = { + setMtu, + defaultAsyncSuccessHandler, + }; + + runChain(aOptions, COMMAND_CHAIN, defaultAsyncFailureHandler); + return CommandResult::Pending(); +} + +void NetworkUtils::sendBroadcastMessage(uint32_t code, char* reason) +{ + NetworkResultOptions result; + switch(code) { + case NETD_COMMAND_INTERFACE_CHANGE: + result.mTopic = NS_ConvertUTF8toUTF16("netd-interface-change"); + break; + case NETD_COMMAND_BANDWIDTH_CONTROLLER: + result.mTopic = NS_ConvertUTF8toUTF16("netd-bandwidth-control"); + break; + default: + return; + } + + result.mBroadcast = true; + result.mReason = NS_ConvertUTF8toUTF16(reason); + postMessage(result); +} + +inline uint32_t NetworkUtils::netdResponseType(uint32_t code) +{ + return (code / 100) * 100; +} + +inline bool NetworkUtils::isBroadcastMessage(uint32_t code) +{ + uint32_t type = netdResponseType(code); + return type == NETD_COMMAND_UNSOLICITED; +} + +inline bool NetworkUtils::isError(uint32_t code) +{ + uint32_t type = netdResponseType(code); + return type != NETD_COMMAND_PROCEEDING && type != NETD_COMMAND_OKAY; +} + +inline bool NetworkUtils::isComplete(uint32_t code) +{ + uint32_t type = netdResponseType(code); + return type != NETD_COMMAND_PROCEEDING; +} + +inline bool NetworkUtils::isProceeding(uint32_t code) +{ + uint32_t type = netdResponseType(code); + return type == NETD_COMMAND_PROCEEDING; +} + +void NetworkUtils::dumpParams(NetworkParams& aOptions, const char* aType) +{ +#ifdef _DEBUG + NU_DBG("Dump params:"); + NU_DBG(" ifname: %s", GET_CHAR(mIfname)); + NU_DBG(" ip: %s", GET_CHAR(mIp)); + NU_DBG(" link: %s", GET_CHAR(mLink)); + NU_DBG(" prefix: %s", GET_CHAR(mPrefix)); + NU_DBG(" wifiStartIp: %s", GET_CHAR(mWifiStartIp)); + NU_DBG(" wifiEndIp: %s", GET_CHAR(mWifiEndIp)); + NU_DBG(" usbStartIp: %s", GET_CHAR(mUsbStartIp)); + NU_DBG(" usbEndIp: %s", GET_CHAR(mUsbEndIp)); + NU_DBG(" dnsserver1: %s", GET_CHAR(mDns1)); + NU_DBG(" dnsserver2: %s", GET_CHAR(mDns2)); + NU_DBG(" internalIfname: %s", GET_CHAR(mInternalIfname)); + NU_DBG(" externalIfname: %s", GET_CHAR(mExternalIfname)); + if (!strcmp(aType, "WIFI")) { + NU_DBG(" wifictrlinterfacename: %s", GET_CHAR(mWifictrlinterfacename)); + NU_DBG(" ssid: %s", GET_CHAR(mSsid)); + NU_DBG(" security: %s", GET_CHAR(mSecurity)); + NU_DBG(" key: %s", GET_CHAR(mKey)); + } +#endif +} + +#undef GET_CHAR +#undef GET_FIELD diff --git a/dom/system/gonk/NetworkUtils.h b/dom/system/gonk/NetworkUtils.h new file mode 100644 index 000000000..d1af35f09 --- /dev/null +++ b/dom/system/gonk/NetworkUtils.h @@ -0,0 +1,498 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NetworkUtils_h +#define NetworkUtils_h + +#include "nsAutoPtr.h" +#include "nsString.h" +#include "mozilla/dom/NetworkOptionsBinding.h" +#include "mozilla/dom/network/NetUtils.h" +#include "mozilla/ipc/Netd.h" +#include "nsTArray.h" +#include "NetIdManager.h" + +class NetworkParams; +class CommandChain; + +class CommandCallback { +public: + typedef void (*CallbackType)(CommandChain*, bool, + mozilla::dom::NetworkResultOptions& aResult); + + typedef void (*CallbackWrapperType)(CallbackType aOriginalCallback, + CommandChain*, bool, + mozilla::dom::NetworkResultOptions& aResult); + + CommandCallback() + : mCallback(nullptr) + , mCallbackWrapper(nullptr) + { + } + + CommandCallback(CallbackType aCallback) + : mCallback(aCallback) + , mCallbackWrapper(nullptr) + { + } + + CommandCallback(CallbackWrapperType aCallbackWrapper, + CommandCallback aOriginalCallback) + : mCallback(aOriginalCallback.mCallback) + , mCallbackWrapper(aCallbackWrapper) + { + } + + void operator()(CommandChain* aChain, bool aError, + mozilla::dom::NetworkResultOptions& aResult) + { + if (mCallbackWrapper) { + return mCallbackWrapper(mCallback, aChain, aError, aResult); + } + if (mCallback) { + return mCallback(aChain, aError, aResult); + } + } + +private: + CallbackType mCallback; + CallbackWrapperType mCallbackWrapper; +}; + +typedef void (*CommandFunc)(CommandChain*, CommandCallback, + mozilla::dom::NetworkResultOptions& aResult); +typedef void (*MessageCallback)(mozilla::dom::NetworkResultOptions& aResult); +typedef void (*ErrorCallback)(NetworkParams& aOptions, + mozilla::dom::NetworkResultOptions& aResult); + +class NetworkParams +{ +public: + NetworkParams() { + } + + NetworkParams(const mozilla::dom::NetworkCommandOptions& aOther) { + +#define COPY_SEQUENCE_FIELD(prop, type) \ + if (aOther.prop.WasPassed()) { \ + mozilla::dom::Sequence<type > const & currentValue = aOther.prop.InternalValue(); \ + uint32_t length = currentValue.Length(); \ + for (uint32_t idx = 0; idx < length; idx++) { \ + prop.AppendElement(currentValue[idx]); \ + } \ + } + +#define COPY_OPT_STRING_FIELD(prop, defaultValue) \ + if (aOther.prop.WasPassed()) { \ + if (aOther.prop.Value().EqualsLiteral("null")) { \ + prop = defaultValue; \ + } else { \ + prop = aOther.prop.Value(); \ + } \ + } else { \ + prop = defaultValue; \ + } + +#define COPY_OPT_FIELD(prop, defaultValue) \ + if (aOther.prop.WasPassed()) { \ + prop = aOther.prop.Value(); \ + } else { \ + prop = defaultValue; \ + } + +#define COPY_FIELD(prop) prop = aOther.prop; + + COPY_FIELD(mId) + COPY_FIELD(mCmd) + COPY_OPT_STRING_FIELD(mDomain, EmptyString()) + COPY_OPT_STRING_FIELD(mGateway, EmptyString()) + COPY_SEQUENCE_FIELD(mGateways, nsString) + COPY_OPT_STRING_FIELD(mIfname, EmptyString()) + COPY_OPT_STRING_FIELD(mIp, EmptyString()) + COPY_OPT_FIELD(mPrefixLength, 0) + COPY_OPT_STRING_FIELD(mMode, EmptyString()) + COPY_OPT_FIELD(mReport, false) + COPY_OPT_FIELD(mEnabled, false) + COPY_OPT_STRING_FIELD(mWifictrlinterfacename, EmptyString()) + COPY_OPT_STRING_FIELD(mInternalIfname, EmptyString()) + COPY_OPT_STRING_FIELD(mExternalIfname, EmptyString()) + COPY_OPT_FIELD(mEnable, false) + COPY_OPT_STRING_FIELD(mSsid, EmptyString()) + COPY_OPT_STRING_FIELD(mSecurity, EmptyString()) + COPY_OPT_STRING_FIELD(mKey, EmptyString()) + COPY_OPT_STRING_FIELD(mPrefix, EmptyString()) + COPY_OPT_STRING_FIELD(mLink, EmptyString()) + COPY_SEQUENCE_FIELD(mInterfaceList, nsString) + COPY_OPT_STRING_FIELD(mWifiStartIp, EmptyString()) + COPY_OPT_STRING_FIELD(mWifiEndIp, EmptyString()) + COPY_OPT_STRING_FIELD(mUsbStartIp, EmptyString()) + COPY_OPT_STRING_FIELD(mUsbEndIp, EmptyString()) + COPY_OPT_STRING_FIELD(mDns1, EmptyString()) + COPY_OPT_STRING_FIELD(mDns2, EmptyString()) + COPY_SEQUENCE_FIELD(mDnses, nsString) + COPY_OPT_STRING_FIELD(mStartIp, EmptyString()) + COPY_OPT_STRING_FIELD(mEndIp, EmptyString()) + COPY_OPT_STRING_FIELD(mServerIp, EmptyString()) + COPY_OPT_STRING_FIELD(mMaskLength, EmptyString()) + COPY_OPT_STRING_FIELD(mPreInternalIfname, EmptyString()) + COPY_OPT_STRING_FIELD(mPreExternalIfname, EmptyString()) + COPY_OPT_STRING_FIELD(mCurInternalIfname, EmptyString()) + COPY_OPT_STRING_FIELD(mCurExternalIfname, EmptyString()) + COPY_OPT_FIELD(mThreshold, -1) + COPY_OPT_FIELD(mIpaddr, 0) + COPY_OPT_FIELD(mMask, 0) + COPY_OPT_FIELD(mGateway_long, 0) + COPY_OPT_FIELD(mDns1_long, 0) + COPY_OPT_FIELD(mDns2_long, 0) + COPY_OPT_FIELD(mMtu, 0) + + mLoopIndex = 0; + +#undef COPY_SEQUENCE_FIELD +#undef COPY_OPT_STRING_FIELD +#undef COPY_OPT_FIELD +#undef COPY_FIELD + } + + // Followings attributes are 1-to-1 mapping to NetworkCommandOptions. + int32_t mId; + nsString mCmd; + nsString mDomain; + nsString mGateway; + nsTArray<nsString> mGateways; + nsString mIfname; + nsString mIp; + uint32_t mPrefixLength; + nsString mMode; + bool mReport; + bool mEnabled; + nsString mWifictrlinterfacename; + nsString mInternalIfname; + nsString mExternalIfname; + bool mEnable; + nsString mSsid; + nsString mSecurity; + nsString mKey; + nsString mPrefix; + nsString mLink; + nsTArray<nsString> mInterfaceList; + nsString mWifiStartIp; + nsString mWifiEndIp; + nsString mUsbStartIp; + nsString mUsbEndIp; + nsString mDns1; + nsString mDns2; + nsTArray<nsString> mDnses; + nsString mStartIp; + nsString mEndIp; + nsString mServerIp; + nsString mMaskLength; + nsString mPreInternalIfname; + nsString mPreExternalIfname; + nsString mCurInternalIfname; + nsString mCurExternalIfname; + long long mThreshold; + long mIpaddr; + long mMask; + long mGateway_long; + long mDns1_long; + long mDns2_long; + long mMtu; + + // Auxiliary information required to carry accros command chain. + int mNetId; // A locally defined id per interface. + uint32_t mLoopIndex; // Loop index for adding/removing multiple gateways. +}; + +// CommandChain store the necessary information to execute command one by one. +// Including : +// 1. Command parameters. +// 2. Command list. +// 3. Error callback function. +// 4. Index of current execution command. +class CommandChain final +{ +public: + CommandChain(const NetworkParams& aParams, + const CommandFunc aCmds[], + uint32_t aLength, + ErrorCallback aError) + : mIndex(-1) + , mParams(aParams) + , mCommands(aCmds) + , mLength(aLength) + , mError(aError) { + } + + NetworkParams& + getParams() + { + return mParams; + }; + + CommandFunc + getNextCommand() + { + mIndex++; + return mIndex < mLength ? mCommands[mIndex] : nullptr; + }; + + ErrorCallback + getErrorCallback() const + { + return mError; + }; + +private: + uint32_t mIndex; + NetworkParams mParams; + const CommandFunc* mCommands; + uint32_t mLength; + ErrorCallback mError; +}; + +// A helper class to easily construct a resolved +// or a pending result for command execution. +class CommandResult +{ +public: + struct Pending {}; + +public: + CommandResult(int32_t aResultCode); + CommandResult(const mozilla::dom::NetworkResultOptions& aResult); + CommandResult(const Pending&); + bool isPending() const; + + mozilla::dom::NetworkResultOptions mResult; + +private: + bool mIsPending; +}; + +class NetworkUtils final +{ +public: + NetworkUtils(MessageCallback aCallback); + ~NetworkUtils(); + + void ExecuteCommand(NetworkParams aOptions); + void onNetdMessage(mozilla::ipc::NetdCommand* aCommand); + + MessageCallback getMessageCallback() { return mMessageCallback; } + +private: + /** + * Commands supported by NetworkUtils. + */ + CommandResult configureInterface(NetworkParams& aOptions); + CommandResult dhcpRequest(NetworkParams& aOptions); + CommandResult stopDhcp(NetworkParams& aOptions); + CommandResult enableInterface(NetworkParams& aOptions); + CommandResult disableInterface(NetworkParams& aOptions); + CommandResult resetConnections(NetworkParams& aOptions); + CommandResult setDefaultRoute(NetworkParams& aOptions); + CommandResult addHostRoute(NetworkParams& aOptions); + CommandResult removeDefaultRoute(NetworkParams& aOptions); + CommandResult removeHostRoute(NetworkParams& aOptions); + CommandResult removeNetworkRoute(NetworkParams& aOptions); + CommandResult setDNS(NetworkParams& aOptions); + CommandResult addSecondaryRoute(NetworkParams& aOptions); + CommandResult removeSecondaryRoute(NetworkParams& aOptions); + CommandResult setNetworkInterfaceAlarm(NetworkParams& aOptions); + CommandResult enableNetworkInterfaceAlarm(NetworkParams& aOptions); + CommandResult disableNetworkInterfaceAlarm(NetworkParams& aOptions); + CommandResult setTetheringAlarm(NetworkParams& aOptions); + CommandResult removeTetheringAlarm(NetworkParams& aOptions); + CommandResult getTetheringStatus(NetworkParams& aOptions); + CommandResult setWifiOperationMode(NetworkParams& aOptions); + CommandResult setDhcpServer(NetworkParams& aOptions); + CommandResult setWifiTethering(NetworkParams& aOptions); + CommandResult setUSBTethering(NetworkParams& aOptions); + CommandResult enableUsbRndis(NetworkParams& aOptions); + CommandResult updateUpStream(NetworkParams& aOptions); + CommandResult createNetwork(NetworkParams& aOptions); + CommandResult destroyNetwork(NetworkParams& aOptions); + CommandResult getNetId(NetworkParams& aOptions); + CommandResult setMtu(NetworkParams& aOptions); + CommandResult getInterfaces(NetworkParams& aOptions); + CommandResult getInterfaceConfig(NetworkParams& aOptions); + CommandResult setInterfaceConfig(NetworkParams& aOptions); + + CommandResult addHostRouteLegacy(NetworkParams& aOptions); + CommandResult removeHostRouteLegacy(NetworkParams& aOptions); + CommandResult setDefaultRouteLegacy(NetworkParams& aOptions); + CommandResult removeDefaultRouteLegacy(NetworkParams& aOptions); + CommandResult removeNetworkRouteLegacy(NetworkParams& aOptions); + + + /** + * function pointer array holds all netd commands should be executed + * in sequence to accomplish a given command by other module. + */ + static const CommandFunc sWifiEnableChain[]; + static const CommandFunc sWifiDisableChain[]; + static const CommandFunc sWifiFailChain[]; + static const CommandFunc sWifiRetryChain[]; + static const CommandFunc sWifiOperationModeChain[]; + static const CommandFunc sUSBEnableChain[]; + static const CommandFunc sUSBDisableChain[]; + static const CommandFunc sUSBFailChain[]; + static const CommandFunc sUpdateUpStreamChain[]; + static const CommandFunc sStartDhcpServerChain[]; + static const CommandFunc sStopDhcpServerChain[]; + static const CommandFunc sNetworkInterfaceEnableAlarmChain[]; + static const CommandFunc sNetworkInterfaceDisableAlarmChain[]; + static const CommandFunc sNetworkInterfaceSetAlarmChain[]; + static const CommandFunc sTetheringInterfaceSetAlarmChain[]; + static const CommandFunc sTetheringInterfaceRemoveAlarmChain[]; + static const CommandFunc sTetheringGetStatusChain[]; + static const CommandFunc sGetInterfacesChain[]; + static const CommandFunc sGetInterfaceConfigChain[]; + static const CommandFunc sSetInterfaceConfigChain[]; + + /** + * Individual netd command stored in command chain. + */ +#define PARAMS CommandChain* aChain, CommandCallback aCallback, \ + mozilla::dom::NetworkResultOptions& aResult + static void wifiFirmwareReload(PARAMS); + static void startAccessPointDriver(PARAMS); + static void stopAccessPointDriver(PARAMS); + static void setAccessPoint(PARAMS); + static void cleanUpStream(PARAMS); + static void createUpStream(PARAMS); + static void startSoftAP(PARAMS); + static void stopSoftAP(PARAMS); + static void clearWifiTetherParms(PARAMS); + static void enableAlarm(PARAMS); + static void disableAlarm(PARAMS); + static void setQuota(PARAMS); + static void removeQuota(PARAMS); + static void setAlarm(PARAMS); + static void removeAlarm(PARAMS); + static void setGlobalAlarm(PARAMS); + static void removeGlobalAlarm(PARAMS); + static void tetherInterface(PARAMS); + static void addInterfaceToLocalNetwork(PARAMS); + static void addRouteToLocalNetwork(PARAMS); + static void preTetherInterfaceList(PARAMS); + static void postTetherInterfaceList(PARAMS); + static void addUpstreamInterface(PARAMS); + static void removeUpstreamInterface(PARAMS); + static void setIpForwardingEnabled(PARAMS); + static void tetheringStatus(PARAMS); + static void stopTethering(PARAMS); + static void startTethering(PARAMS); + static void untetherInterface(PARAMS); + static void removeInterfaceFromLocalNetwork(PARAMS); + static void setDnsForwarders(PARAMS); + static void enableNat(PARAMS); + static void disableNat(PARAMS); + static void setDefaultInterface(PARAMS); + static void setInterfaceDns(PARAMS); + static void getInterfaceList(PARAMS); + static void getConfig(PARAMS); + static void setConfig(PARAMS); + static void wifiTetheringSuccess(PARAMS); + static void usbTetheringSuccess(PARAMS); + static void networkInterfaceAlarmSuccess(PARAMS); + static void updateUpStreamSuccess(PARAMS); + static void setDhcpServerSuccess(PARAMS); + static void wifiOperationModeSuccess(PARAMS); + static void clearAddrForInterface(PARAMS); + static void createNetwork(PARAMS); + static void destroyNetwork(PARAMS); + static void addInterfaceToNetwork(PARAMS); + static void addDefaultRouteToNetwork(PARAMS); + static void setDefaultNetwork(PARAMS); + static void removeDefaultRoute(PARAMS); + static void removeNetworkRouteSuccess(PARAMS); + static void removeNetworkRoute(PARAMS); + static void addRouteToInterface(PARAMS); + static void removeRouteFromInterface(PARAMS); + static void modifyRouteOnInterface(PARAMS, bool aDoAdd); + static void enableIpv6(PARAMS); + static void disableIpv6(PARAMS); + static void setMtu(PARAMS); + static void setIpv6Enabled(PARAMS, bool aEnabled); + static void addRouteToSecondaryTable(PARAMS); + static void removeRouteFromSecondaryTable(PARAMS); + static void defaultAsyncSuccessHandler(PARAMS); + static void getInterfacesSuccess(PARAMS); + static void getInterfaceConfigSuccess(PARAMS); + static void setInterfaceConfigSuccess(PARAMS); + +#undef PARAMS + + /** + * Error callback function executed when a command is fail. + */ +#define PARAMS NetworkParams& aOptions, \ + mozilla::dom::NetworkResultOptions& aResult + static void wifiTetheringFail(PARAMS); + static void wifiOperationModeFail(PARAMS); + static void usbTetheringFail(PARAMS); + static void updateUpStreamFail(PARAMS); + static void setDhcpServerFail(PARAMS); + static void networkInterfaceAlarmFail(PARAMS); + static void setDnsFail(PARAMS); + static void defaultAsyncFailureHandler(PARAMS); + static void getInterfacesFail(PARAMS); + static void getInterfaceConfigFail(PARAMS); + static void setInterfaceConfigFail(PARAMS); + +#undef PARAMS + + /** + * Command chain processing functions. + */ + static void next(CommandChain* aChain, bool aError, + mozilla::dom::NetworkResultOptions& aResult); + static void nextNetdCommand(); + static void doCommand(const char* aCommand, CommandChain* aChain, CommandCallback aCallback); + + /** + * Notify broadcast message to main thread. + */ + void sendBroadcastMessage(uint32_t code, char* reason); + + /** + * Utility functions. + */ + CommandResult checkUsbRndisState(NetworkParams& aOptions); + void dumpParams(NetworkParams& aOptions, const char* aType); + + static void escapeQuote(nsCString& aString); + inline uint32_t netdResponseType(uint32_t code); + inline bool isBroadcastMessage(uint32_t code); + inline bool isError(uint32_t code); + inline bool isComplete(uint32_t code); + inline bool isProceeding(uint32_t code); + void Shutdown(); + static void runNextQueuedCommandChain(); + static void finalizeSuccess(CommandChain* aChain, + mozilla::dom::NetworkResultOptions& aResult); + + template<size_t N> + static void runChain(const NetworkParams& aParams, + const CommandFunc (&aCmds)[N], + ErrorCallback aError); + + static nsCString getSubnetIp(const nsCString& aIp, int aPrefixLength); + + /** + * Callback function to send netd result to main thread. + */ + MessageCallback mMessageCallback; + + /* + * Utility class to access libnetutils. + */ + nsAutoPtr<NetUtils> mNetUtils; + + NetIdManager mNetIdManager; +}; + +#endif diff --git a/dom/system/gonk/NetworkWorker.cpp b/dom/system/gonk/NetworkWorker.cpp new file mode 100644 index 000000000..caf07f375 --- /dev/null +++ b/dom/system/gonk/NetworkWorker.cpp @@ -0,0 +1,271 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "NetworkWorker.h" +#include "NetworkUtils.h" +#include <nsThreadUtils.h> +#include "mozilla/ModuleUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsAutoPtr.h" +#include "nsXULAppAPI.h" + +#define NS_NETWORKWORKER_CID \ + { 0x6df093e1, 0x8127, 0x4fa7, {0x90, 0x13, 0xa3, 0xaa, 0xa7, 0x79, 0xbb, 0xdd} } + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +namespace mozilla { + +nsCOMPtr<nsIThread> gWorkerThread; + +// The singleton network worker, to be used on the main thread. +StaticRefPtr<NetworkWorker> gNetworkWorker; + +// The singleton networkutils class, that can be used on any thread. +static nsAutoPtr<NetworkUtils> gNetworkUtils; + +// Runnable used dispatch command result on the main thread. +class NetworkResultDispatcher : public Runnable +{ +public: + NetworkResultDispatcher(const NetworkResultOptions& aResult) + : mResult(aResult) + { + MOZ_ASSERT(!NS_IsMainThread()); + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + if (gNetworkWorker) { + gNetworkWorker->DispatchNetworkResult(mResult); + } + return NS_OK; + } +private: + NetworkResultOptions mResult; +}; + +// Runnable used dispatch netd command on the worker thread. +class NetworkCommandDispatcher : public Runnable +{ +public: + NetworkCommandDispatcher(const NetworkParams& aParams) + : mParams(aParams) + { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(!NS_IsMainThread()); + + if (gNetworkUtils) { + gNetworkUtils->ExecuteCommand(mParams); + } + return NS_OK; + } +private: + NetworkParams mParams; +}; + +// Runnable used dispatch netd result on the worker thread. +class NetdEventRunnable : public Runnable +{ +public: + NetdEventRunnable(NetdCommand* aCommand) + : mCommand(aCommand) + { + MOZ_ASSERT(!NS_IsMainThread()); + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(!NS_IsMainThread()); + + if (gNetworkUtils) { + gNetworkUtils->onNetdMessage(mCommand); + } + return NS_OK; + } + +private: + nsAutoPtr<NetdCommand> mCommand; +}; + +class NetdMessageConsumer : public NetdConsumer +{ +public: + NetdMessageConsumer() + { + MOZ_ASSERT(NS_IsMainThread()); + } + + void MessageReceived(NetdCommand* aCommand) + { + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIRunnable> runnable = new NetdEventRunnable(aCommand); + if (gWorkerThread) { + gWorkerThread->Dispatch(runnable, nsIEventTarget::DISPATCH_NORMAL); + } + } +}; + +NS_IMPL_ISUPPORTS(NetworkWorker, nsINetworkWorker) + +NetworkWorker::NetworkWorker() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gNetworkWorker); +} + +NetworkWorker::~NetworkWorker() +{ + MOZ_ASSERT(!gNetworkWorker); + MOZ_ASSERT(!mListener); +} + +already_AddRefed<NetworkWorker> +NetworkWorker::FactoryCreate() +{ + if (!XRE_IsParentProcess()) { + return nullptr; + } + + MOZ_ASSERT(NS_IsMainThread()); + + if (!gNetworkWorker) { + gNetworkWorker = new NetworkWorker(); + ClearOnShutdown(&gNetworkWorker); + + gNetworkUtils = new NetworkUtils(NetworkWorker::NotifyResult); + ClearOnShutdown(&gNetworkUtils); + } + + RefPtr<NetworkWorker> worker = gNetworkWorker.get(); + return worker.forget(); +} + +NS_IMETHODIMP +NetworkWorker::Start(nsINetworkEventListener* aListener) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aListener); + + if (mListener) { + return NS_OK; + } + + nsresult rv; + + rv = NS_NewNamedThread("NetworkWorker", getter_AddRefs(gWorkerThread)); + if (NS_FAILED(rv)) { + NS_WARNING("Can't create network control thread"); + return NS_ERROR_FAILURE; + } + + StartNetd(new NetdMessageConsumer()); + mListener = aListener; + + return NS_OK; +} + +NS_IMETHODIMP +NetworkWorker::Shutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!mListener) { + return NS_OK; + } + + StopNetd(); + + gWorkerThread->Shutdown(); + gWorkerThread = nullptr; + + mListener = nullptr; + return NS_OK; +} + +// Receive command from main thread (NetworkService.js). +NS_IMETHODIMP +NetworkWorker::PostMessage(JS::Handle<JS::Value> aOptions, JSContext* aCx) +{ + MOZ_ASSERT(NS_IsMainThread()); + + NetworkCommandOptions options; + if (!options.Init(aCx, aOptions)) { + NS_WARNING("Bad dictionary passed to NetworkWorker::SendCommand"); + return NS_ERROR_FAILURE; + } + + // Dispatch the command to the control thread. + NetworkParams NetworkParams(options); + nsCOMPtr<nsIRunnable> runnable = new NetworkCommandDispatcher(NetworkParams); + if (gWorkerThread) { + gWorkerThread->Dispatch(runnable, nsIEventTarget::DISPATCH_NORMAL); + } + return NS_OK; +} + +void +NetworkWorker::DispatchNetworkResult(const NetworkResultOptions& aOptions) +{ + MOZ_ASSERT(NS_IsMainThread()); + + mozilla::AutoSafeJSContext cx; + JS::RootedValue val(cx); + + if (!ToJSValue(cx, aOptions, &val)) { + return; + } + + // Call the listener with a JS value. + if (mListener) { + mListener->OnEvent(val); + } +} + +// Callback function from network worker thread to update result on main thread. +void +NetworkWorker::NotifyResult(NetworkResultOptions& aResult) +{ + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIRunnable> runnable = new NetworkResultDispatcher(aResult); + NS_DispatchToMainThread(runnable); +} + +NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(NetworkWorker, + NetworkWorker::FactoryCreate) + +NS_DEFINE_NAMED_CID(NS_NETWORKWORKER_CID); + +static const mozilla::Module::CIDEntry kNetworkWorkerCIDs[] = { + { &kNS_NETWORKWORKER_CID, false, nullptr, NetworkWorkerConstructor }, + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kNetworkWorkerContracts[] = { + { "@mozilla.org/network/worker;1", &kNS_NETWORKWORKER_CID }, + { nullptr } +}; + +static const mozilla::Module kNetworkWorkerModule = { + mozilla::Module::kVersion, + kNetworkWorkerCIDs, + kNetworkWorkerContracts, + nullptr +}; + +} // namespace mozilla + +NSMODULE_DEFN(NetworkWorkerModule) = &kNetworkWorkerModule; diff --git a/dom/system/gonk/NetworkWorker.h b/dom/system/gonk/NetworkWorker.h new file mode 100644 index 000000000..f5c0a8fdd --- /dev/null +++ b/dom/system/gonk/NetworkWorker.h @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NetworkWorker_h +#define NetworkWorker_h + +#include "mozilla/dom/NetworkOptionsBinding.h" +#include "mozilla/ipc/Netd.h" +#include "nsINetworkWorker.h" +#include "nsCOMPtr.h" +#include "nsThread.h" + +namespace mozilla { + +class NetworkWorker final : public nsINetworkWorker +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSINETWORKWORKER + + static already_AddRefed<NetworkWorker> FactoryCreate(); + + void DispatchNetworkResult(const mozilla::dom::NetworkResultOptions& aOptions); + +private: + NetworkWorker(); + ~NetworkWorker(); + + static void NotifyResult(mozilla::dom::NetworkResultOptions& aResult); + + nsCOMPtr<nsINetworkEventListener> mListener; +}; + +} // namespace mozilla + +#endif // NetworkWorker_h diff --git a/dom/system/gonk/OpenFileFinder.cpp b/dom/system/gonk/OpenFileFinder.cpp new file mode 100644 index 000000000..388e813e1 --- /dev/null +++ b/dom/system/gonk/OpenFileFinder.cpp @@ -0,0 +1,251 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "OpenFileFinder.h" + +#include "mozilla/FileUtils.h" +#include "nsPrintfCString.h" + +#include <sys/stat.h> +#include <errno.h> + +#undef USE_DEBUG +#define USE_DEBUG 0 + +#undef LOG +#undef LOGW +#undef ERR +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "OpenFileFinder", ## args) +#define LOGW(args...) __android_log_print(ANDROID_LOG_WARN, "OpenFileFinder", ## args) +#define ERR(args...) __android_log_print(ANDROID_LOG_ERROR, "OpenFileFinder", ## args) + +#undef DBG +#if USE_DEBUG +#define DBG(args...) __android_log_print(ANDROID_LOG_DEBUG, "OpenFileFinder" , ## args) +#else +#define DBG(args...) +#endif + +namespace mozilla { +namespace system { + +OpenFileFinder::OpenFileFinder(const nsACString& aPath, + bool aCheckIsB2gOrDescendant /* = true */) + : mPath(aPath), + mProcDir(nullptr), + mFdDir(nullptr), + mPid(0), + mCheckIsB2gOrDescendant(aCheckIsB2gOrDescendant) +{ + // We assume that we're running in the parent process + mMyPid = getpid(); +} + +OpenFileFinder::~OpenFileFinder() +{ + Close(); +} + +bool +OpenFileFinder::First(OpenFileFinder::Info* aInfo) +{ + Close(); + + mProcDir = opendir("/proc"); + if (!mProcDir) { + return false; + } + mState = NEXT_PID; + return Next(aInfo); +} + +bool +OpenFileFinder::Next(OpenFileFinder::Info* aInfo) +{ + // NOTE: This function calls readdir and readlink, neither of which should + // block since we're using the proc filesystem, which is a purely + // kernel in-memory filesystem and doesn't depend on external driver + // behaviour. + while (mState != DONE) { + switch (mState) { + case NEXT_PID: { + struct dirent *pidEntry; + pidEntry = readdir(mProcDir); + if (!pidEntry) { + mState = DONE; + break; + } + char *endPtr; + mPid = strtol(pidEntry->d_name, &endPtr, 10); + if (mPid == 0 || *endPtr != '\0') { + // Not a +ve number - ignore + continue; + } + // We've found a /proc/PID directory. Scan open file descriptors. + if (mFdDir) { + closedir(mFdDir); + } + nsPrintfCString fdDirPath("/proc/%d/fd", mPid); + mFdDir = opendir(fdDirPath.get()); + if (!mFdDir) { + continue; + } + mState = CHECK_FDS; + } + // Fall through + case CHECK_FDS: { + struct dirent *fdEntry; + while((fdEntry = readdir(mFdDir))) { + if (!strcmp(fdEntry->d_name, ".") || + !strcmp(fdEntry->d_name, "..")) { + continue; + } + nsPrintfCString fdSymLink("/proc/%d/fd/%s", mPid, fdEntry->d_name); + nsCString resolvedPath; + if (ReadSymLink(fdSymLink, resolvedPath) && PathMatches(resolvedPath)) { + // We found an open file contained within the directory tree passed + // into the constructor. + FillInfo(aInfo, resolvedPath); + // If sCheckIsB2gOrDescendant is set false, the caller cares about + // all processes which have open files. If sCheckIsB2gOrDescendant + // is set false, we only care about the b2g proccess or its descendants. + if (!mCheckIsB2gOrDescendant || aInfo->mIsB2gOrDescendant) { + return true; + } + LOG("Ignore process(%d), not a b2g process or its descendant.", + aInfo->mPid); + } + } + // We've checked all of the files for this pid, move onto the next one. + mState = NEXT_PID; + continue; + } + case DONE: + default: + mState = DONE; // covers the default case + break; + } + } + return false; +} + +void +OpenFileFinder::Close() +{ + if (mFdDir) { + closedir(mFdDir); + } + if (mProcDir) { + closedir(mProcDir); + } +} + +void +OpenFileFinder::FillInfo(OpenFileFinder::Info* aInfo, const nsACString& aPath) +{ + aInfo->mFileName = aPath; + aInfo->mPid = mPid; + nsPrintfCString exePath("/proc/%d/exe", mPid); + ReadSymLink(exePath, aInfo->mExe); + aInfo->mComm.Truncate(); + aInfo->mAppName.Truncate(); + nsPrintfCString statPath("/proc/%d/stat", mPid); + nsCString statString; + statString.SetLength(200); + char *stat = statString.BeginWriting(); + if (!stat) { + return; + } + ReadSysFile(statPath.get(), stat, statString.Length()); + // The stat line includes the comm field, surrounded by parenthesis. + // However, the contents of the comm field itself is arbitrary and + // and can include ')', so we search for the rightmost ) as being + // the end of the comm field. + char *closeParen = strrchr(stat, ')'); + if (!closeParen) { + return; + } + char *openParen = strchr(stat, '('); + if (!openParen) { + return; + } + if (openParen >= closeParen) { + return; + } + nsDependentCSubstring comm(&openParen[1], closeParen - openParen - 1); + aInfo->mComm = comm; + // There is a single character field after the comm and then + // the parent pid (the field we're interested in). + // ) X ppid + // 01234 + int ppid = atoi(&closeParen[4]); + + if (mPid == mMyPid) { + // This is chrome process + aInfo->mIsB2gOrDescendant = true; + DBG("Chrome process has open file(s)"); + return; + } + // For the rest (non-chrome process), we recursively check the ppid to know + // it is a descendant of b2g or not. See bug 931456. + while (ppid != mMyPid && ppid != 1) { + DBG("Process(%d) is not forked from b2g(%d) or Init(1), keep looking", + ppid, mMyPid); + nsPrintfCString ppStatPath("/proc/%d/stat", ppid); + ReadSysFile(ppStatPath.get(), stat, statString.Length()); + closeParen = strrchr(stat, ')'); + if (!closeParen) { + return; + } + ppid = atoi(&closeParen[4]); + } + if (ppid == 1) { + // This is a not a b2g process. + DBG("Non-b2g process has open file(s)"); + aInfo->mIsB2gOrDescendant = false; + return; + } + if (ppid == mMyPid) { + // This is a descendant of b2g. + DBG("Child process of chrome process has open file(s)"); + aInfo->mIsB2gOrDescendant = true; + } + + // This looks like a content process. The comm field will be the + // app name. + aInfo->mAppName = aInfo->mComm; +} + +bool +OpenFileFinder::ReadSymLink(const nsACString& aSymLink, nsACString& aOutPath) +{ + aOutPath.Truncate(); + const char *symLink = aSymLink.BeginReading(); + + // Verify that we actually have a symlink. + struct stat st; + if (lstat(symLink, &st)) { + return false; + } + if ((st.st_mode & S_IFMT) != S_IFLNK) { + return false; + } + + // Contrary to the documentation st.st_size doesn't seem to be a reliable + // indication of the length when reading from /proc, so we use a fixed + // size buffer instead. + + char resolvedSymLink[PATH_MAX]; + ssize_t pathLength = readlink(symLink, resolvedSymLink, + sizeof(resolvedSymLink) - 1); + if (pathLength <= 0) { + return false; + } + resolvedSymLink[pathLength] = '\0'; + aOutPath.Assign(resolvedSymLink); + return true; +} + +} // system +} // mozilla diff --git a/dom/system/gonk/OpenFileFinder.h b/dom/system/gonk/OpenFileFinder.h new file mode 100644 index 000000000..24517965a --- /dev/null +++ b/dom/system/gonk/OpenFileFinder.h @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_openfilefinder_h__ +#define mozilla_system_openfilefinder_h__ + +#include "nsString.h" + +#include <dirent.h> + +namespace mozilla { +namespace system { + +class OpenFileFinder +{ +public: + enum State + { + NEXT_PID, + CHECK_FDS, + DONE + }; + class Info + { + public: + nsCString mFileName; // name of the the open file + nsCString mAppName; // App which has the file open (if it's a b2g app) + pid_t mPid; // pid of the process which has the file open + nsCString mComm; // comm associated with pid + nsCString mExe; // executable name associated with pid + bool mIsB2gOrDescendant; // this is b2g/its descendant or not + }; + + OpenFileFinder(const nsACString& aPath, bool aCheckIsB2gOrDescendant = true); + ~OpenFileFinder(); + + bool First(Info* aInfo); // Return the first open file + bool Next(Info* aInfo); // Return the next open file + void Close(); + +private: + + void FillInfo(Info *aInfo, const nsACString& aPath); + bool ReadSymLink(const nsACString& aSymLink, nsACString& aOutPath); + bool PathMatches(const nsACString& aPath) + { + return Substring(aPath, 0, mPath.Length()).Equals(mPath); + } + + State mState; // Keeps track of what we're doing. + nsCString mPath; // Only report files contained within this directory tree + DIR* mProcDir; // Used for scanning /proc + DIR* mFdDir; // Used for scanning /proc/PID/fd + int mPid; // PID currently being processed + pid_t mMyPid; // PID of parent process, we assume we're running on it. + bool mCheckIsB2gOrDescendant; // Do we care about non-b2g process? +}; + +} // system +} // mozilla + +#endif // mozilla_system_nsvolume_h__ diff --git a/dom/system/gonk/RILSystemMessenger.jsm b/dom/system/gonk/RILSystemMessenger.jsm new file mode 100644 index 000000000..81373458c --- /dev/null +++ b/dom/system/gonk/RILSystemMessenger.jsm @@ -0,0 +1,338 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "RIL", function () { + let obj = {}; + Cu.import("resource://gre/modules/ril_consts.js", obj); + return obj; +}); + +/** + * RILSystemMessenger + */ +this.RILSystemMessenger = function() {}; +RILSystemMessenger.prototype = { + + /** + * Hook of Broadcast function + * + * @param aType + * The type of the message to be sent. + * @param aMessage + * The message object to be broadcasted. + */ + broadcastMessage: function(aType, aMessage) { + // Function stub to be replaced by the owner of this messenger. + }, + + /** + * Hook of the function to create MozStkCommand message. + * @param aStkProactiveCmd + * nsIStkProactiveCmd instance. + * + * @return a JS object which complies the dictionary of MozStkCommand defined + * in MozStkCommandEvent.webidl + */ + createCommandMessage: function(aStkProactiveCmd) { + // Function stub to be replaced by the owner of this messenger. + }, + + /** + * Wrapper to send "telephony-new-call" system message. + */ + notifyNewCall: function() { + this.broadcastMessage("telephony-new-call", {}); + }, + + /** + * Wrapper to send "telephony-call-ended" system message. + */ + notifyCallEnded: function(aServiceId, aNumber, aCdmaWaitingNumber, aEmergency, + aDuration, aOutgoing, aHangUpLocal) { + let data = { + serviceId: aServiceId, + number: aNumber, + emergency: aEmergency, + duration: aDuration, + direction: aOutgoing ? "outgoing" : "incoming", + hangUpLocal: aHangUpLocal + }; + + if (aCdmaWaitingNumber != null) { + data.secondNumber = aCdmaWaitingNumber; + } + + this.broadcastMessage("telephony-call-ended", data); + }, + + _convertSmsMessageClass: function(aMessageClass) { + return RIL.GECKO_SMS_MESSAGE_CLASSES[aMessageClass] || null; + }, + + _convertSmsDelivery: function(aDelivery) { + return ["received", "sending", "sent", "error"][aDelivery] || null; + }, + + _convertSmsDeliveryStatus: function(aDeliveryStatus) { + return [ + RIL.GECKO_SMS_DELIVERY_STATUS_NOT_APPLICABLE, + RIL.GECKO_SMS_DELIVERY_STATUS_SUCCESS, + RIL.GECKO_SMS_DELIVERY_STATUS_PENDING, + RIL.GECKO_SMS_DELIVERY_STATUS_ERROR + ][aDeliveryStatus] || null; + }, + + /** + * Wrapper to send 'sms-received', 'sms-delivery-success', 'sms-sent', + * 'sms-failed', 'sms-delivery-error' system message. + */ + notifySms: function(aNotificationType, aId, aThreadId, aIccId, aDelivery, + aDeliveryStatus, aSender, aReceiver, aBody, aMessageClass, + aTimestamp, aSentTimestamp, aDeliveryTimestamp, aRead) { + let msgType = [ + "sms-received", + "sms-sent", + "sms-delivery-success", + "sms-failed", + "sms-delivery-error" + ][aNotificationType]; + + if (!msgType) { + throw new Error("Invalid Notification Type: " + aNotificationType); + } + + this.broadcastMessage(msgType, { + iccId: aIccId, + type: "sms", + id: aId, + threadId: aThreadId, + delivery: this._convertSmsDelivery(aDelivery), + deliveryStatus: this._convertSmsDeliveryStatus(aDeliveryStatus), + sender: aSender, + receiver: aReceiver, + body: aBody, + messageClass: this._convertSmsMessageClass(aMessageClass), + timestamp: aTimestamp, + sentTimestamp: aSentTimestamp, + deliveryTimestamp: aDeliveryTimestamp, + read: aRead + }); + }, + + _convertCbGsmGeographicalScope: function(aGeographicalScope) { + return RIL.CB_GSM_GEOGRAPHICAL_SCOPE_NAMES[aGeographicalScope] || null; + }, + + _convertCbMessageClass: function(aMessageClass) { + return RIL.GECKO_SMS_MESSAGE_CLASSES[aMessageClass] || null; + }, + + _convertCbEtwsWarningType: function(aWarningType) { + return RIL.CB_ETWS_WARNING_TYPE_NAMES[aWarningType] || null; + }, + + /** + * Wrapper to send 'cellbroadcast-received' system message. + */ + notifyCbMessageReceived: function(aServiceId, aGsmGeographicalScope, aMessageCode, + aMessageId, aLanguage, aBody, aMessageClass, + aTimestamp, aCdmaServiceCategory, aHasEtwsInfo, + aEtwsWarningType, aEtwsEmergencyUserAlert, aEtwsPopup) { + // Align the same layout to MozCellBroadcastMessage + let data = { + serviceId: aServiceId, + gsmGeographicalScope: this._convertCbGsmGeographicalScope(aGsmGeographicalScope), + messageCode: aMessageCode, + messageId: aMessageId, + language: aLanguage, + body: aBody, + messageClass: this._convertCbMessageClass(aMessageClass), + timestamp: aTimestamp, + cdmaServiceCategory: null, + etws: null + }; + + if (aHasEtwsInfo) { + data.etws = { + warningType: this._convertCbEtwsWarningType(aEtwsWarningType), + emergencyUserAlert: aEtwsEmergencyUserAlert, + popup: aEtwsPopup + }; + } + + if (aCdmaServiceCategory != + Ci.nsICellBroadcastService.CDMA_SERVICE_CATEGORY_INVALID) { + data.cdmaServiceCategory = aCdmaServiceCategory; + } + + this.broadcastMessage("cellbroadcast-received", data); + }, + + /** + * Wrapper to send 'ussd-received' system message. + */ + notifyUssdReceived: function(aServiceId, aMessage, aSessionEnded) { + this.broadcastMessage("ussd-received", { + serviceId: aServiceId, + message: aMessage, + sessionEnded: aSessionEnded + }); + }, + + /** + * Wrapper to send 'cdma-info-rec-received' system message with Display Info. + */ + notifyCdmaInfoRecDisplay: function(aServiceId, aDisplay) { + this.broadcastMessage("cdma-info-rec-received", { + clientId: aServiceId, + display: aDisplay + }); + }, + + /** + * Wrapper to send 'cdma-info-rec-received' system message with Called Party + * Number Info. + */ + notifyCdmaInfoRecCalledPartyNumber: function(aServiceId, aType, aPlan, + aNumber, aPi, aSi) { + this.broadcastMessage("cdma-info-rec-received", { + clientId: aServiceId, + calledNumber: { + type: aType, + plan: aPlan, + number: aNumber, + pi: aPi, + si: aSi + } + }); + }, + + /** + * Wrapper to send 'cdma-info-rec-received' system message with Calling Party + * Number Info. + */ + notifyCdmaInfoRecCallingPartyNumber: function(aServiceId, aType, aPlan, + aNumber, aPi, aSi) { + this.broadcastMessage("cdma-info-rec-received", { + clientId: aServiceId, + callingNumber: { + type: aType, + plan: aPlan, + number: aNumber, + pi: aPi, + si: aSi + } + }); + }, + + /** + * Wrapper to send 'cdma-info-rec-received' system message with Connected Party + * Number Info. + */ + notifyCdmaInfoRecConnectedPartyNumber: function(aServiceId, aType, aPlan, + aNumber, aPi, aSi) { + this.broadcastMessage("cdma-info-rec-received", { + clientId: aServiceId, + connectedNumber: { + type: aType, + plan: aPlan, + number: aNumber, + pi: aPi, + si: aSi + } + }); + }, + + /** + * Wrapper to send 'cdma-info-rec-received' system message with Signal Info. + */ + notifyCdmaInfoRecSignal: function(aServiceId, aType, aAlertPitch, aSignal) { + this.broadcastMessage("cdma-info-rec-received", { + clientId: aServiceId, + signal: { + type: aType, + alertPitch: aAlertPitch, + signal: aSignal + } + }); + }, + + /** + * Wrapper to send 'cdma-info-rec-received' system message with Redirecting + * Number Info. + */ + notifyCdmaInfoRecRedirectingNumber: function(aServiceId, aType, aPlan, + aNumber, aPi, aSi, aReason) { + this.broadcastMessage("cdma-info-rec-received", { + clientId: aServiceId, + redirect: { + type: aType, + plan: aPlan, + number: aNumber, + pi: aPi, + si: aSi, + reason: aReason + } + }); + }, + + /** + * Wrapper to send 'cdma-info-rec-received' system message with Line Control Info. + */ + notifyCdmaInfoRecLineControl: function(aServiceId, aPolarityIncluded, + aToggle, aReverse, aPowerDenial) { + this.broadcastMessage("cdma-info-rec-received", { + clientId: aServiceId, + lineControl: { + polarityIncluded: aPolarityIncluded, + toggle: aToggle, + reverse: aReverse, + powerDenial: aPowerDenial + } + }); + }, + + /** + * Wrapper to send 'cdma-info-rec-received' system message with CLIR Info. + */ + notifyCdmaInfoRecClir: function(aServiceId, aCause) { + this.broadcastMessage("cdma-info-rec-received", { + clientId: aServiceId, + clirCause: aCause + }); + }, + + /** + * Wrapper to send 'cdma-info-rec-received' system message with Audio Control Info. + */ + notifyCdmaInfoRecAudioControl: function(aServiceId, aUpLink, aDownLink) { + this.broadcastMessage("cdma-info-rec-received", { + clientId: aServiceId, + audioControl: { + upLink: aUpLink, + downLink: aDownLink + } + }); + }, + + /** + * Wrapper to send 'icc-stkcommand' system message with Audio Control Info. + */ + notifyStkProactiveCommand: function(aIccId, aCommand) { + this.broadcastMessage("icc-stkcommand", { + iccId: aIccId, + command: this.createCommandMessage(aCommand) + }); + } +}; + +this.EXPORTED_SYMBOLS = [ + 'RILSystemMessenger' +]; diff --git a/dom/system/gonk/RILSystemMessengerHelper.js b/dom/system/gonk/RILSystemMessengerHelper.js new file mode 100644 index 000000000..6ef263b66 --- /dev/null +++ b/dom/system/gonk/RILSystemMessengerHelper.js @@ -0,0 +1,169 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var RSM = {}; +Cu.import("resource://gre/modules/RILSystemMessenger.jsm", RSM); + +const RILSYSTEMMESSENGERHELPER_CONTRACTID = + "@mozilla.org/ril/system-messenger-helper;1"; +const RILSYSTEMMESSENGERHELPER_CID = + Components.ID("{19d9a4ea-580d-11e4-8f6c-37ababfaaea9}"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger", + "@mozilla.org/system-message-internal;1", + "nsISystemMessagesInternal"); + +XPCOMUtils.defineLazyServiceGetter(this, "gStkCmdFactory", + "@mozilla.org/icc/stkcmdfactory;1", + "nsIStkCmdFactory"); + +var DEBUG = false; +function debug(s) { + dump("-@- RILSystemMessenger: " + s + "\n"); +}; + +// Read debug setting from pref. +try { + let debugPref = Services.prefs.getBoolPref("ril.debugging.enabled"); + DEBUG = DEBUG || debugPref; +} catch (e) {} + +/** + * RILSystemMessengerHelper + */ +function RILSystemMessengerHelper() { + this.messenger = new RSM.RILSystemMessenger(); + this.messenger.broadcastMessage = (aType, aMessage) => { + if (DEBUG) { + debug("broadcastMessage: aType: " + aType + + ", aMessage: "+ JSON.stringify(aMessage)); + } + + gSystemMessenger.broadcastMessage(aType, aMessage); + }; + + this.messenger.createCommandMessage = (aStkProactiveCmd) => { + return gStkCmdFactory.createCommandMessage(aStkProactiveCmd); + }; +} +RILSystemMessengerHelper.prototype = { + + classID: RILSYSTEMMESSENGERHELPER_CID, + QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyMessenger, + Ci.nsISmsMessenger, + Ci.nsICellbroadcastMessenger, + Ci.nsIMobileConnectionMessenger, + Ci.nsIIccMessenger]), + + /** + * RILSystemMessenger instance. + */ + messenger: null, + + /** + * nsITelephonyMessenger API + */ + notifyNewCall: function() { + this.messenger.notifyNewCall(); + }, + + notifyCallEnded: function(aServiceId, aNumber, aCdmaWaitingNumber, aEmergency, + aDuration, aOutgoing, aHangUpLocal) { + this.messenger.notifyCallEnded(aServiceId, aNumber, aCdmaWaitingNumber, aEmergency, + aDuration, aOutgoing, aHangUpLocal); + }, + + notifyUssdReceived: function(aServiceId, aMessage, aSessionEnded) { + this.messenger.notifyUssdReceived(aServiceId, aMessage, aSessionEnded); + }, + + /** + * nsISmsMessenger API + */ + notifySms: function(aNotificationType, aId, aThreadId, aIccId, aDelivery, + aDeliveryStatus, aSender, aReceiver, aBody, aMessageClass, + aTimestamp, aSentTimestamp, aDeliveryTimestamp, aRead) { + this.messenger.notifySms(aNotificationType, aId, aThreadId, aIccId, aDelivery, + aDeliveryStatus, aSender, aReceiver, aBody, aMessageClass, + aTimestamp, aSentTimestamp, aDeliveryTimestamp, aRead); + }, + + /** + * nsICellbroadcastMessenger API + */ + notifyCbMessageReceived: function(aServiceId, aGsmGeographicalScope, aMessageCode, + aMessageId, aLanguage, aBody, aMessageClass, + aTimestamp, aCdmaServiceCategory, aHasEtwsInfo, + aEtwsWarningType, aEtwsEmergencyUserAlert, aEtwsPopup) { + this.messenger.notifyCbMessageReceived(aServiceId, aGsmGeographicalScope, aMessageCode, + aMessageId, aLanguage, aBody, aMessageClass, + aTimestamp, aCdmaServiceCategory, aHasEtwsInfo, + aEtwsWarningType, aEtwsEmergencyUserAlert, aEtwsPopup); + }, + + /** + * nsIMobileConnectionMessenger API + */ + notifyCdmaInfoRecDisplay: function(aServiceId, aDisplay) { + this.messenger.notifyCdmaInfoRecDisplay(aServiceId, aDisplay); + }, + + notifyCdmaInfoRecCalledPartyNumber: function(aServiceId, aType, aPlan, + aNumber, aPi, aSi) { + this.messenger.notifyCdmaInfoRecCalledPartyNumber(aServiceId, aType, aPlan, + aNumber, aPi, aSi); + }, + + notifyCdmaInfoRecCallingPartyNumber: function(aServiceId, aType, aPlan, + aNumber, aPi, aSi) { + this.messenger.notifyCdmaInfoRecCallingPartyNumber(aServiceId, aType, aPlan, + aNumber, aPi, aSi); + }, + + notifyCdmaInfoRecConnectedPartyNumber: function(aServiceId, aType, aPlan, + aNumber, aPi, aSi) { + this.messenger.notifyCdmaInfoRecConnectedPartyNumber(aServiceId, aType, aPlan, + aNumber, aPi, aSi); + }, + + notifyCdmaInfoRecSignal: function(aServiceId, aType, aAlertPitch, aSignal) { + this.messenger.notifyCdmaInfoRecSignal(aServiceId, aType, aAlertPitch, aSignal); + }, + + notifyCdmaInfoRecRedirectingNumber: function(aServiceId, aType, aPlan, + aNumber, aPi, aSi, aReason) { + this.messenger.notifyCdmaInfoRecRedirectingNumber(aServiceId, aType, aPlan, + aNumber, aPi, aSi, aReason); + }, + + notifyCdmaInfoRecLineControl: function(aServiceId, aPolarityIncluded, + aToggle, aReverse, aPowerDenial) { + this.messenger.notifyCdmaInfoRecLineControl(aServiceId, aPolarityIncluded, + aToggle, aReverse, aPowerDenial); + }, + + notifyCdmaInfoRecClir: function(aServiceId, aCause) { + this.messenger.notifyCdmaInfoRecClir(aServiceId, aCause); + }, + + notifyCdmaInfoRecAudioControl: function(aServiceId, aUpLink, aDownLink) { + this.messenger.notifyCdmaInfoRecAudioControl(aServiceId, aUpLink, aDownLink); + }, + + /** + * nsIIccMessenger API + */ + notifyStkProactiveCommand: function(aIccId, aCommand) { + this.messenger.notifyStkProactiveCommand(aIccId, aCommand); + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RILSystemMessengerHelper]); diff --git a/dom/system/gonk/RILSystemMessengerHelper.manifest b/dom/system/gonk/RILSystemMessengerHelper.manifest new file mode 100644 index 000000000..7d1943702 --- /dev/null +++ b/dom/system/gonk/RILSystemMessengerHelper.manifest @@ -0,0 +1,6 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +component {19d9a4ea-580d-11e4-8f6c-37ababfaaea9} RILSystemMessengerHelper.js +contract @mozilla.org/ril/system-messenger-helper;1 {19d9a4ea-580d-11e4-8f6c-37ababfaaea9}
\ No newline at end of file diff --git a/dom/system/gonk/RadioInterfaceLayer.js b/dom/system/gonk/RadioInterfaceLayer.js new file mode 100644 index 000000000..f5885db5d --- /dev/null +++ b/dom/system/gonk/RadioInterfaceLayer.js @@ -0,0 +1,1324 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Sntp.jsm"); +Cu.import("resource://gre/modules/systemlibs.js"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "RIL", function () { + let obj = {}; + Cu.import("resource://gre/modules/ril_consts.js", obj); + return obj; +}); + +// Ril quirk to always turn the radio off for the client without SIM card +// except hw default client. +var RILQUIRKS_RADIO_OFF_WO_CARD = + libcutils.property_get("ro.moz.ril.radio_off_wo_card", "false") == "true"; + +const RADIOINTERFACELAYER_CID = + Components.ID("{2d831c8d-6017-435b-a80c-e5d422810cea}"); +const RADIOINTERFACE_CID = + Components.ID("{6a7c91f0-a2b3-4193-8562-8969296c0b54}"); + +const NS_XPCOM_SHUTDOWN_OBSERVER_ID = "xpcom-shutdown"; +const kNetworkConnStateChangedTopic = "network-connection-state-changed"; +const kMozSettingsChangedObserverTopic = "mozsettings-changed"; +const kSysMsgListenerReadyObserverTopic = "system-message-listener-ready"; +const kSysClockChangeObserverTopic = "system-clock-change"; +const kScreenStateChangedTopic = "screen-state-changed"; + +const kSettingsClockAutoUpdateEnabled = "time.clock.automatic-update.enabled"; +const kSettingsClockAutoUpdateAvailable = "time.clock.automatic-update.available"; +const kSettingsTimezoneAutoUpdateEnabled = "time.timezone.automatic-update.enabled"; +const kSettingsTimezoneAutoUpdateAvailable = "time.timezone.automatic-update.available"; + +const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; + +const kPrefRilNumRadioInterfaces = "ril.numRadioInterfaces"; +const kPrefRilDebuggingEnabled = "ril.debugging.enabled"; + +const RADIO_POWER_OFF_TIMEOUT = 30000; +const HW_DEFAULT_CLIENT_ID = 0; + +const NETWORK_TYPE_WIFI = Ci.nsINetworkInfo.NETWORK_TYPE_WIFI; +const NETWORK_TYPE_MOBILE = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE; + +// set to true in ril_consts.js to see debug messages +var DEBUG = RIL.DEBUG_RIL; + +function updateDebugFlag() { + // Read debug setting from pref + let debugPref; + try { + debugPref = Services.prefs.getBoolPref(kPrefRilDebuggingEnabled); + } catch (e) { + debugPref = false; + } + DEBUG = RIL.DEBUG_RIL || debugPref; +} +updateDebugFlag(); + +function debug(s) { + dump("-*- RadioInterfaceLayer: " + s + "\n"); +} + +XPCOMUtils.defineLazyServiceGetter(this, "gIccService", + "@mozilla.org/icc/gonkiccservice;1", + "nsIGonkIccService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gMobileMessageService", + "@mozilla.org/mobilemessage/mobilemessageservice;1", + "nsIMobileMessageService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSmsService", + "@mozilla.org/sms/gonksmsservice;1", + "nsIGonkSmsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageBroadcaster"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager", + "@mozilla.org/network/manager;1", + "nsINetworkManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gTimeService", + "@mozilla.org/time/timeservice;1", + "nsITimeService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSystemWorkerManager", + "@mozilla.org/telephony/system-worker-manager;1", + "nsISystemWorkerManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gTelephonyService", + "@mozilla.org/telephony/telephonyservice;1", + "nsIGonkTelephonyService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gMobileConnectionService", + "@mozilla.org/mobileconnection/mobileconnectionservice;1", + "nsIGonkMobileConnectionService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gCellBroadcastService", + "@mozilla.org/cellbroadcast/cellbroadcastservice;1", + "nsIGonkCellBroadcastService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gDataCallManager", + "@mozilla.org/datacall/manager;1", + "nsIDataCallManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gDataCallInterfaceService", + "@mozilla.org/datacall/interfaceservice;1", + "nsIGonkDataCallInterfaceService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gStkCmdFactory", + "@mozilla.org/icc/stkcmdfactory;1", + "nsIStkCmdFactory"); + +XPCOMUtils.defineLazyGetter(this, "gRadioEnabledController", function() { + let _ril = null; + let _pendingMessages = []; // For queueing "setRadioEnabled" message. + let _isProcessingPending = false; + let _timer = null; + let _request = null; + let _deactivatingDeferred = {}; + let _initializedCardState = {}; + let _allCardStateInitialized = !RILQUIRKS_RADIO_OFF_WO_CARD; + + return { + init: function(ril) { + _ril = ril; + }, + + receiveCardState: function(clientId) { + if (_allCardStateInitialized) { + return; + } + + if (DEBUG) debug("RadioControl: receive cardState from " + clientId); + _initializedCardState[clientId] = true; + if (Object.keys(_initializedCardState).length == _ril.numRadioInterfaces) { + _allCardStateInitialized = true; + this._startProcessingPending(); + } + }, + + setRadioEnabled: function(clientId, data, callback) { + if (DEBUG) debug("setRadioEnabled: " + clientId + ": " + JSON.stringify(data)); + let message = { + clientId: clientId, + data: data, + callback: callback + }; + _pendingMessages.push(message); + this._startProcessingPending(); + }, + + notifyRadioStateChanged: function(clientId, radioState) { + gMobileConnectionService.notifyRadioStateChanged(clientId, radioState); + }, + + _startProcessingPending: function() { + if (!_isProcessingPending) { + if (DEBUG) debug("RadioControl: start dequeue"); + _isProcessingPending = true; + this._processNextMessage(); + } + }, + + _processNextMessage: function() { + if (_pendingMessages.length === 0 || !_allCardStateInitialized) { + if (DEBUG) debug("RadioControl: stop dequeue"); + _isProcessingPending = false; + return; + } + + let msg = _pendingMessages.shift(); + this._handleMessage(msg); + }, + + _getNumCards: function() { + let numCards = 0; + for (let i = 0, N = _ril.numRadioInterfaces; i < N; ++i) { + if (_ril.getRadioInterface(i).isCardPresent()) { + numCards++; + } + } + return numCards; + }, + + _isRadioAbleToEnableAtClient: function(clientId, numCards) { + if (!RILQUIRKS_RADIO_OFF_WO_CARD) { + return true; + } + + // We could only turn on the radio for clientId if + // 1. a SIM card is presented or + // 2. it is the default clientId and there is no any SIM card at any client. + + if (_ril.getRadioInterface(clientId).isCardPresent()) { + return true; + } + + numCards = numCards == null ? this._getNumCards() : numCards; + if (clientId === HW_DEFAULT_CLIENT_ID && numCards === 0) { + return true; + } + + return false; + }, + + _handleMessage: function(message) { + if (DEBUG) debug("RadioControl: handleMessage: " + JSON.stringify(message)); + let clientId = message.clientId || 0; + let connection = + gMobileConnectionService.getItemByServiceId(clientId); + let radioState = connection && connection.radioState; + + if (message.data.enabled) { + if (this._isRadioAbleToEnableAtClient(clientId)) { + this._setRadioEnabledInternal(message); + } else { + // Not really do it but respond success. + message.callback(message.data); + } + + this._processNextMessage(); + } else { + _request = this._setRadioEnabledInternal.bind(this, message); + + // In 2G network, modem takes 35+ seconds to process deactivate data + // call request if device has active voice call (please see bug 964974 + // for more details). Therefore we should hangup all active voice calls + // first. And considering some DSDS architecture, toggling one radio may + // toggle both, so we send hangUpAll to all clients. + let hangUpCallback = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyCallback]), + notifySuccess: function() {}, + notifyError: function() {} + }; + + gTelephonyService.enumerateCalls({ + QueryInterface: XPCOMUtils.generateQI([Ci.nsITelephonyListener]), + enumerateCallState: function(aInfo) { + gTelephonyService.hangUpCall(aInfo.clientId, aInfo.callIndex, + hangUpCallback); + }, + enumerateCallStateComplete: function() {} + }); + + // In some DSDS architecture with only one modem, toggling one radio may + // toggle both. Therefore, for safely turning off, we should first + // explicitly deactivate all data calls from all clients. + this._deactivateDataCalls().then(() => { + if (DEBUG) debug("RadioControl: deactivation done"); + this._executeRequest(); + }); + + this._createTimer(); + } + }, + + _setRadioEnabledInternal: function(message) { + let clientId = message.clientId || 0; + let enabled = message.data.enabled || false; + let radioInterface = _ril.getRadioInterface(clientId); + + radioInterface.workerMessenger.send("setRadioEnabled", message.data, + (function(response) { + if (response.errorMsg) { + // If request fails, set current radio state to unknown, since we will + // handle it in |mobileConnectionService|. + this.notifyRadioStateChanged(clientId, + Ci.nsIMobileConnection.MOBILE_RADIO_STATE_UNKNOWN); + } + return message.callback(response); + }).bind(this)); + }, + + _deactivateDataCalls: function() { + if (DEBUG) debug("RadioControl: deactivating data calls..."); + _deactivatingDeferred = {}; + + let promise = Promise.resolve(); + for (let i = 0, N = _ril.numRadioInterfaces; i < N; ++i) { + promise = promise.then(this._deactivateDataCallsForClient(i)); + } + + return promise; + }, + + _deactivateDataCallsForClient: function(clientId) { + return function() { + let deferred = _deactivatingDeferred[clientId] = Promise.defer(); + let dataCallHandler = gDataCallManager.getDataCallHandler(clientId); + + dataCallHandler.deactivateDataCalls(function() { + deferred.resolve(); + }); + + return deferred.promise; + }; + }, + + _createTimer: function() { + if (!_timer) { + _timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + _timer.initWithCallback(this._executeRequest.bind(this), + RADIO_POWER_OFF_TIMEOUT, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + _cancelTimer: function() { + if (_timer) { + _timer.cancel(); + } + }, + + _executeRequest: function() { + if (typeof _request === "function") { + if (DEBUG) debug("RadioControl: executeRequest"); + this._cancelTimer(); + _request(); + _request = null; + } + this._processNextMessage(); + }, + }; +}); + +// Initialize shared preference "ril.numRadioInterfaces" according to system +// property. +try { + Services.prefs.setIntPref(kPrefRilNumRadioInterfaces, (function() { + // When Gonk property "ro.moz.ril.numclients" is not set, return 1; if + // explicitly set to any number larger-equal than 0, return num; else, return + // 1 for compatibility. + try { + let numString = libcutils.property_get("ro.moz.ril.numclients", "1"); + let num = parseInt(numString, 10); + if (num >= 0) { + return num; + } + } catch (e) {} + + return 1; + })()); +} catch (e) {} + +function DataCall(aAttributes) { + for (let key in aAttributes) { + if (key === "pdpType") { + // Convert pdp type into constant int value. + this[key] = RIL.RIL_DATACALL_PDP_TYPES.indexOf(aAttributes[key]); + continue; + } + + this[key] = aAttributes[key]; + } +} +DataCall.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDataCall]), + + failCause: Ci.nsIDataCallInterface.DATACALL_FAIL_NONE, + suggestedRetryTime: -1, + cid: -1, + active: -1, + pdpType: -1, + ifname: null, + addreses: null, + dnses: null, + gateways: null, + pcscf: null, + mtu: -1 +}; + +function RadioInterfaceLayer() { + let workerMessenger = new WorkerMessenger(); + workerMessenger.init(); + this.setWorkerDebugFlag = workerMessenger.setDebugFlag.bind(workerMessenger); + + let numIfaces = this.numRadioInterfaces; + if (DEBUG) debug(numIfaces + " interfaces"); + this.radioInterfaces = []; + for (let clientId = 0; clientId < numIfaces; clientId++) { + this.radioInterfaces.push(new RadioInterface(clientId, workerMessenger)); + } + + Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + Services.prefs.addObserver(kPrefRilDebuggingEnabled, this, false); + + gRadioEnabledController.init(this); +} +RadioInterfaceLayer.prototype = { + + classID: RADIOINTERFACELAYER_CID, + classInfo: XPCOMUtils.generateCI({classID: RADIOINTERFACELAYER_CID, + classDescription: "RadioInterfaceLayer", + interfaces: [Ci.nsIRadioInterfaceLayer]}), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRadioInterfaceLayer, + Ci.nsIObserver]), + + /** + * nsIObserver interface methods. + */ + + observe: function(subject, topic, data) { + switch (topic) { + case NS_XPCOM_SHUTDOWN_OBSERVER_ID: + for (let radioInterface of this.radioInterfaces) { + radioInterface.shutdown(); + } + this.radioInterfaces = null; + Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + break; + + case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID: + if (data === kPrefRilDebuggingEnabled) { + updateDebugFlag(); + this.setWorkerDebugFlag(DEBUG); + } + break; + } + }, + + /** + * nsIRadioInterfaceLayer interface methods. + */ + + getRadioInterface: function(clientId) { + return this.radioInterfaces[clientId]; + }, + + setMicrophoneMuted: function(muted) { + for (let clientId = 0; clientId < this.numRadioInterfaces; clientId++) { + let radioInterface = this.radioInterfaces[clientId]; + radioInterface.workerMessenger.send("setMute", { muted: muted }); + } + } +}; + +XPCOMUtils.defineLazyGetter(RadioInterfaceLayer.prototype, + "numRadioInterfaces", function() { + try { + return Services.prefs.getIntPref(kPrefRilNumRadioInterfaces); + } catch(e) {} + + return 1; +}); + +function WorkerMessenger() { + // Initial owning attributes. + this.radioInterfaces = []; + this.tokenCallbackMap = {}; + + this.worker = new ChromeWorker("resource://gre/modules/ril_worker.js"); + this.worker.onerror = this.onerror.bind(this); + this.worker.onmessage = this.onmessage.bind(this); +} +WorkerMessenger.prototype = { + radioInterfaces: null, + worker: null, + + // This gets incremented each time we send out a message. + token: 1, + + // Maps tokens we send out with messages to the message callback. + tokenCallbackMap: null, + + init: function() { + let options = { + debug: DEBUG, + quirks: { + callstateExtraUint32: + libcutils.property_get("ro.moz.ril.callstate_extra_int", "false") === "true", + requestUseDialEmergencyCall: + libcutils.property_get("ro.moz.ril.dial_emergency_call", "false") === "true", + simAppStateExtraFields: + libcutils.property_get("ro.moz.ril.simstate_extra_field", "false") === "true", + extraUint2ndCall: + libcutils.property_get("ro.moz.ril.extra_int_2nd_call", "false") === "true", + haveQueryIccLockRetryCount: + libcutils.property_get("ro.moz.ril.query_icc_count", "false") === "true", + sendStkProfileDownload: + libcutils.property_get("ro.moz.ril.send_stk_profile_dl", "false") === "true", + smscAddressFormat: + libcutils.property_get("ro.moz.ril.smsc_address_format", "text"), + dataRegistrationOnDemand: + libcutils.property_get("ro.moz.ril.data_reg_on_demand", "false") === "true", + subscriptionControl: + libcutils.property_get("ro.moz.ril.subscription_control", "false") === "true", + signalExtraInt: + libcutils.property_get("ro.moz.ril.signal_extra_int", "false") === "true", + availableNetworkExtraStr: + libcutils.property_get("ro.moz.ril.avlbl_nw_extra_str", "false") === "true", + } + }; + + this.send(null, "setInitialOptions", options); + }, + + setDebugFlag: function(aDebug) { + let options = { debug: aDebug }; + this.send(null, "setDebugFlag", options); + }, + + debug: function(aClientId, aMessage) { + // We use the same debug subject with RadioInterface's here. + dump("-*- RadioInterface[" + aClientId + "]: " + aMessage + "\n"); + }, + + onerror: function(event) { + if (DEBUG) { + this.debug("X", "Got an error: " + event.filename + ":" + + event.lineno + ": " + event.message + "\n"); + } + event.preventDefault(); + }, + + /** + * Process the incoming message from the RIL worker. + */ + onmessage: function(event) { + let message = event.data; + let clientId = message.rilMessageClientId; + if (clientId === null) { + return; + } + + if (DEBUG) { + this.debug(clientId, "Received message from worker: " + JSON.stringify(message)); + } + + let token = message.rilMessageToken; + if (token == null) { + // That's an unsolicited message. Pass to RadioInterface directly. + let radioInterface = this.radioInterfaces[clientId]; + radioInterface.handleUnsolicitedWorkerMessage(message); + return; + } + + let callback = this.tokenCallbackMap[message.rilMessageToken]; + if (!callback) { + if (DEBUG) this.debug(clientId, "Ignore orphan token: " + message.rilMessageToken); + return; + } + + let keep = false; + try { + keep = callback(message); + } catch(e) { + if (DEBUG) this.debug(clientId, "callback throws an exception: " + e); + } + + if (!keep) { + delete this.tokenCallbackMap[message.rilMessageToken]; + } + }, + + registerClient: function(aClientId, aRadioInterface) { + if (DEBUG) this.debug(aClientId, "Starting RIL Worker"); + + // Keep a reference so that we can dispatch unsolicited messages to it. + this.radioInterfaces[aClientId] = aRadioInterface; + + this.send(null, "registerClient", { clientId: aClientId }); + gSystemWorkerManager.registerRilWorker(aClientId, this.worker); + }, + + /** + * Send arbitrary message to worker. + * + * @param rilMessageType + * A text message type. + * @param message [optional] + * An optional message object to send. + * @param callback [optional] + * An optional callback function which is called when worker replies + * with an message containing a "rilMessageToken" attribute of the + * same value we passed. This callback function accepts only one + * parameter -- the reply from worker. It also returns a boolean + * value true to keep current token-callback mapping and wait for + * another worker reply, or false to remove the mapping. + */ + send: function(clientId, rilMessageType, message, callback) { + message = message || {}; + + message.rilMessageClientId = clientId; + message.rilMessageToken = this.token; + this.token++; + + if (callback) { + // Only create the map if callback is provided. For sending a request + // and intentionally leaving the callback undefined, that reply will + // be dropped in |this.onmessage| because of that orphan token. + // + // For sending a request that never replied at all, we're fine with this + // because no callback shall be passed and we leave nothing to be cleaned + // up later. + this.tokenCallbackMap[message.rilMessageToken] = callback; + } + + message.rilMessageType = rilMessageType; + this.worker.postMessage(message); + } +}; + +function RadioInterface(aClientId, aWorkerMessenger) { + this.clientId = aClientId; + this.workerMessenger = { + send: aWorkerMessenger.send.bind(aWorkerMessenger, aClientId) + }; + aWorkerMessenger.registerClient(aClientId, this); + + this.operatorInfo = {}; + + let lock = gSettingsService.createLock(); + + // Read the "time.clock.automatic-update.enabled" setting to see if + // we need to adjust the system clock time by NITZ or SNTP. + lock.get(kSettingsClockAutoUpdateEnabled, this); + + // Read the "time.timezone.automatic-update.enabled" setting to see if + // we need to adjust the system timezone by NITZ. + lock.get(kSettingsTimezoneAutoUpdateEnabled, this); + + // Set "time.clock.automatic-update.available" to false when starting up. + this.setClockAutoUpdateAvailable(false); + + // Set "time.timezone.automatic-update.available" to false when starting up. + this.setTimezoneAutoUpdateAvailable(false); + + Services.obs.addObserver(this, kMozSettingsChangedObserverTopic, false); + Services.obs.addObserver(this, kSysClockChangeObserverTopic, false); + Services.obs.addObserver(this, kScreenStateChangedTopic, false); + + Services.obs.addObserver(this, kNetworkConnStateChangedTopic, false); + + this._sntp = new Sntp(this.setClockBySntp.bind(this), + Services.prefs.getIntPref("network.sntp.maxRetryCount"), + Services.prefs.getIntPref("network.sntp.refreshPeriod"), + Services.prefs.getIntPref("network.sntp.timeout"), + Services.prefs.getCharPref("network.sntp.pools").split(";"), + Services.prefs.getIntPref("network.sntp.port")); +} + +RadioInterface.prototype = { + + classID: RADIOINTERFACE_CID, + classInfo: XPCOMUtils.generateCI({classID: RADIOINTERFACE_CID, + classDescription: "RadioInterface", + interfaces: [Ci.nsIRadioInterface]}), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRadioInterface, + Ci.nsIObserver, + Ci.nsISettingsServiceCallback]), + + // A private wrapped WorkerMessenger instance. + workerMessenger: null, + + debug: function(s) { + dump("-*- RadioInterface[" + this.clientId + "]: " + s + "\n"); + }, + + shutdown: function() { + Services.obs.removeObserver(this, kMozSettingsChangedObserverTopic); + Services.obs.removeObserver(this, kSysClockChangeObserverTopic); + Services.obs.removeObserver(this, kScreenStateChangedTopic); + Services.obs.removeObserver(this, kNetworkConnStateChangedTopic); + }, + + isCardPresent: function() { + let icc = gIccService.getIccByServiceId(this.clientId); + let cardState = icc ? icc.cardState : Ci.nsIIcc.CARD_STATE_UNKNOWN; + return cardState !== Ci.nsIIcc.CARD_STATE_UNDETECTED && + cardState !== Ci.nsIIcc.CARD_STATE_UNKNOWN; + }, + + handleUnsolicitedWorkerMessage: function(message) { + switch (message.rilMessageType) { + case "callRing": + gTelephonyService.notifyCallRing(); + break; + case "currentCalls": + gTelephonyService.notifyCurrentCalls(this.clientId, message.calls); + break; + case "cdmaCallWaiting": + gTelephonyService.notifyCdmaCallWaiting(this.clientId, + message.waitingCall); + break; + case "suppSvcNotification": + gTelephonyService.notifySupplementaryService(this.clientId, + message.number, + message.notification); + break; + case "ussdreceived": + gTelephonyService.notifyUssdReceived(this.clientId, message.message, + message.sessionEnded); + break; + case "datacalllistchanged": + let dataCalls = message.datacalls.map(dataCall => new DataCall(dataCall)); + gDataCallInterfaceService.notifyDataCallListChanged(this.clientId, + dataCalls.length, + dataCalls); + break; + case "emergencyCbModeChange": + gMobileConnectionService.notifyEmergencyCallbackModeChanged(this.clientId, + message.active, + message.timeoutMs); + break; + case "networkinfochanged": + gMobileConnectionService.notifyNetworkInfoChanged(this.clientId, + message); + break; + case "networkselectionmodechange": + gMobileConnectionService.notifyNetworkSelectModeChanged(this.clientId, + message.mode); + break; + case "voiceregistrationstatechange": + gMobileConnectionService.notifyVoiceInfoChanged(this.clientId, message); + break; + case "dataregistrationstatechange": + gMobileConnectionService.notifyDataInfoChanged(this.clientId, message); + break; + case "signalstrengthchange": + gMobileConnectionService.notifySignalStrengthChanged(this.clientId, + message); + break; + case "operatorchange": + gMobileConnectionService.notifyOperatorChanged(this.clientId, message); + break; + case "otastatuschange": + gMobileConnectionService.notifyOtaStatusChanged(this.clientId, message.status); + break; + case "deviceidentitieschange": + gMobileConnectionService.notifyDeviceIdentitiesChanged(this.clientId, + message.deviceIdentities.imei, + message.deviceIdentities.imeisv, + message.deviceIdentities.esn, + message.deviceIdentities.meid); + break; + case "radiostatechange": + // gRadioEnabledController should know the radio state for each client, + // so notify gRadioEnabledController here. + gRadioEnabledController.notifyRadioStateChanged(this.clientId, + message.radioState); + break; + case "cardstatechange": + gIccService.notifyCardStateChanged(this.clientId, + message.cardState); + gRadioEnabledController.receiveCardState(this.clientId); + break; + case "sms-received": + this.handleSmsReceived(message); + break; + case "cellbroadcast-received": + this.handleCellbroadcastMessageReceived(message); + break; + case "nitzTime": + this.handleNitzTime(message); + break; + case "iccinfochange": + gIccService.notifyIccInfoChanged(this.clientId, + message.iccid ? message : null); + break; + case "iccimsi": + gIccService.notifyImsiChanged(this.clientId, message.imsi); + break; + case "iccmbdn": + this.handleIccMbdn(message); + break; + case "iccmwis": + this.handleIccMwis(message.mwi); + break; + case "stkcommand": + gIccService.notifyStkCommand(this.clientId, + gStkCmdFactory.createCommand(message)); + break; + case "stksessionend": + gIccService.notifyStkSessionEnd(this.clientId); + break; + case "cdma-info-rec-received": + this.handleCdmaInformationRecords(message.records); + break; + default: + throw new Error("Don't know about this message type: " + + message.rilMessageType); + } + }, + + setDataRegistration: function(attach) { + let deferred = Promise.defer(); + this.workerMessenger.send("setDataRegistration", + {attach: attach}, + (function(response) { + // Always resolve to proceed with the following steps. + deferred.resolve(response.errorMsg ? response.errorMsg : null); + }).bind(this)); + + return deferred.promise; + }, + + /** + * TODO: Bug 911713 - B2G NetworkManager: Move policy control logic to + * NetworkManager + */ + updateRILNetworkInterface: function() { + let connHandler = gDataCallManager.getDataCallHandler(this.clientId); + connHandler.updateRILNetworkInterface(); + }, + + /** + * handle received SMS. + */ + handleSmsReceived: function(aMessage) { + let header = aMessage.header; + // Concatenation Info: + // - segmentRef: a modulo 256 counter indicating the reference number for a + // particular concatenated short message. '0' is a valid number. + // - The concatenation info will not be available in |header| if + // segmentSeq or segmentMaxSeq is 0. + // See 3GPP TS 23.040, 9.2.3.24.1 Concatenated Short Messages. + let segmentRef = (header && header.segmentRef !== undefined) + ? header.segmentRef : 1; + let segmentSeq = header && header.segmentSeq || 1; + let segmentMaxSeq = header && header.segmentMaxSeq || 1; + // Application Ports: + // The port number ranges from 0 to 49151. + // see 3GPP TS 23.040, 9.2.3.24.3/4 Application Port Addressing. + let originatorPort = (header && header.originatorPort !== undefined) + ? header.originatorPort + : Ci.nsIGonkSmsService.SMS_APPLICATION_PORT_INVALID; + let destinationPort = (header && header.destinationPort !== undefined) + ? header.destinationPort + : Ci.nsIGonkSmsService.SMS_APPLICATION_PORT_INVALID; + // MWI info: + let mwiPresent = (aMessage.mwi)? true : false; + let mwiDiscard = (mwiPresent)? aMessage.mwi.discard: false; + let mwiMsgCount = (mwiPresent)? aMessage.mwi.msgCount: 0; + let mwiActive = (mwiPresent)? aMessage.mwi.active: false; + // CDMA related attributes: + let cdmaMessageType = aMessage.messageType || 0; + let cdmaTeleservice = aMessage.teleservice || 0; + let cdmaServiceCategory = aMessage.serviceCategory || 0; + + gSmsService + .notifyMessageReceived(this.clientId, + aMessage.SMSC || null, + aMessage.sentTimestamp, + aMessage.sender, + aMessage.pid, + aMessage.encoding, + RIL.GECKO_SMS_MESSAGE_CLASSES + .indexOf(aMessage.messageClass), + aMessage.language || null, + segmentRef, + segmentSeq, + segmentMaxSeq, + originatorPort, + destinationPort, + mwiPresent, + mwiDiscard, + mwiMsgCount, + mwiActive, + cdmaMessageType, + cdmaTeleservice, + cdmaServiceCategory, + aMessage.body || null, + aMessage.data || [], + (aMessage.data) ? aMessage.data.length : 0); + }, + + /** + * Set the setting value of "time.clock.automatic-update.available". + */ + setClockAutoUpdateAvailable: function(value) { + gSettingsService.createLock().set(kSettingsClockAutoUpdateAvailable, value, null); + }, + + /** + * Set the setting value of "time.timezone.automatic-update.available". + */ + setTimezoneAutoUpdateAvailable: function(value) { + gSettingsService.createLock().set(kSettingsTimezoneAutoUpdateAvailable, value, null); + }, + + /** + * Set the system clock by NITZ. + */ + setClockByNitz: function(message) { + // To set the system clock time. Note that there could be a time diff + // between when the NITZ was received and when the time is actually set. + gTimeService.set( + message.networkTimeInMS + (Date.now() - message.receiveTimeInMS)); + }, + + /** + * Set the system time zone by NITZ. + */ + setTimezoneByNitz: function(message) { + // To set the sytem timezone. Note that we need to convert the time zone + // value to a UTC repesentation string in the format of "UTC(+/-)hh:mm". + // Ex, time zone -480 is "UTC+08:00"; time zone 630 is "UTC-10:30". + // + // We can unapply the DST correction if we want the raw time zone offset: + // message.networkTimeZoneInMinutes -= message.networkDSTInMinutes; + if (message.networkTimeZoneInMinutes != (new Date()).getTimezoneOffset()) { + let absTimeZoneInMinutes = Math.abs(message.networkTimeZoneInMinutes); + let timeZoneStr = "UTC"; + timeZoneStr += (message.networkTimeZoneInMinutes > 0 ? "-" : "+"); + timeZoneStr += ("0" + Math.floor(absTimeZoneInMinutes / 60)).slice(-2); + timeZoneStr += ":"; + timeZoneStr += ("0" + absTimeZoneInMinutes % 60).slice(-2); + gSettingsService.createLock().set("time.timezone", timeZoneStr, null); + } + }, + + /** + * Handle the NITZ message. + */ + handleNitzTime: function(message) { + // Got the NITZ info received from the ril_worker. + this.setClockAutoUpdateAvailable(true); + this.setTimezoneAutoUpdateAvailable(true); + + // Cache the latest NITZ message whenever receiving it. + this._lastNitzMessage = message; + + // Set the received NITZ clock if the setting is enabled. + if (this._clockAutoUpdateEnabled) { + this.setClockByNitz(message); + } + // Set the received NITZ timezone if the setting is enabled. + if (this._timezoneAutoUpdateEnabled) { + this.setTimezoneByNitz(message); + } + }, + + /** + * Set the system clock by SNTP. + */ + setClockBySntp: function(offset) { + // Got the SNTP info. + this.setClockAutoUpdateAvailable(true); + if (!this._clockAutoUpdateEnabled) { + return; + } + if (this._lastNitzMessage) { + if (DEBUG) debug("SNTP: NITZ available, discard SNTP"); + return; + } + gTimeService.set(Date.now() + offset); + }, + + handleIccMbdn: function(message) { + let service = Cc["@mozilla.org/voicemail/voicemailservice;1"] + .getService(Ci.nsIGonkVoicemailService); + service.notifyInfoChanged(this.clientId, message.number, message.alphaId); + }, + + handleIccMwis: function(mwi) { + let service = Cc["@mozilla.org/voicemail/voicemailservice;1"] + .getService(Ci.nsIGonkVoicemailService); + // Note: returnNumber and returnMessage is not available from UICC. + service.notifyStatusChanged(this.clientId, mwi.active, mwi.msgCount, + null, null); + }, + + _convertCbGsmGeographicalScope: function(aGeographicalScope) { + return (aGeographicalScope != null) + ? aGeographicalScope + : Ci.nsICellBroadcastService.GSM_GEOGRAPHICAL_SCOPE_INVALID; + }, + + _convertCbMessageClass: function(aMessageClass) { + let index = RIL.GECKO_SMS_MESSAGE_CLASSES.indexOf(aMessageClass); + return (index != -1) + ? index + : Ci.nsICellBroadcastService.GSM_MESSAGE_CLASS_NORMAL; + }, + + _convertCbEtwsWarningType: function(aWarningType) { + return (aWarningType != null) + ? aWarningType + : Ci.nsICellBroadcastService.GSM_ETWS_WARNING_INVALID; + }, + + handleCellbroadcastMessageReceived: function(aMessage) { + let etwsInfo = aMessage.etws; + let hasEtwsInfo = etwsInfo != null; + let serviceCategory = (aMessage.serviceCategory) + ? aMessage.serviceCategory + : Ci.nsICellBroadcastService.CDMA_SERVICE_CATEGORY_INVALID; + + gCellBroadcastService + .notifyMessageReceived(this.clientId, + this._convertCbGsmGeographicalScope(aMessage.geographicalScope), + aMessage.messageCode, + aMessage.messageId, + aMessage.language, + aMessage.fullBody, + this._convertCbMessageClass(aMessage.messageClass), + Date.now(), + serviceCategory, + hasEtwsInfo, + (hasEtwsInfo) + ? this._convertCbEtwsWarningType(etwsInfo.warningType) + : Ci.nsICellBroadcastService.GSM_ETWS_WARNING_INVALID, + hasEtwsInfo ? etwsInfo.emergencyUserAlert : false, + hasEtwsInfo ? etwsInfo.popup : false); + }, + + handleCdmaInformationRecords: function(aRecords) { + if (DEBUG) this.debug("cdma-info-rec-received: " + JSON.stringify(aRecords)); + + let clientId = this.clientId; + + aRecords.forEach(function(aRecord) { + if (aRecord.display) { + gMobileConnectionService + .notifyCdmaInfoRecDisplay(clientId, aRecord.display); + return; + } + + if (aRecord.calledNumber) { + gMobileConnectionService + .notifyCdmaInfoRecCalledPartyNumber(clientId, + aRecord.calledNumber.type, + aRecord.calledNumber.plan, + aRecord.calledNumber.number, + aRecord.calledNumber.pi, + aRecord.calledNumber.si); + return; + } + + if (aRecord.callingNumber) { + gMobileConnectionService + .notifyCdmaInfoRecCallingPartyNumber(clientId, + aRecord.callingNumber.type, + aRecord.callingNumber.plan, + aRecord.callingNumber.number, + aRecord.callingNumber.pi, + aRecord.callingNumber.si); + return; + } + + if (aRecord.connectedNumber) { + gMobileConnectionService + .notifyCdmaInfoRecConnectedPartyNumber(clientId, + aRecord.connectedNumber.type, + aRecord.connectedNumber.plan, + aRecord.connectedNumber.number, + aRecord.connectedNumber.pi, + aRecord.connectedNumber.si); + return; + } + + if (aRecord.signal) { + gMobileConnectionService + .notifyCdmaInfoRecSignal(clientId, + aRecord.signal.type, + aRecord.signal.alertPitch, + aRecord.signal.signal); + return; + } + + if (aRecord.redirect) { + gMobileConnectionService + .notifyCdmaInfoRecRedirectingNumber(clientId, + aRecord.redirect.type, + aRecord.redirect.plan, + aRecord.redirect.number, + aRecord.redirect.pi, + aRecord.redirect.si, + aRecord.redirect.reason); + return; + } + + if (aRecord.lineControl) { + gMobileConnectionService + .notifyCdmaInfoRecLineControl(clientId, + aRecord.lineControl.polarityIncluded, + aRecord.lineControl.toggle, + aRecord.lineControl.reverse, + aRecord.lineControl.powerDenial); + return; + } + + if (aRecord.clirCause) { + gMobileConnectionService + .notifyCdmaInfoRecClir(clientId, + aRecord.clirCause); + return; + } + + if (aRecord.audioControl) { + gMobileConnectionService + .notifyCdmaInfoRecAudioControl(clientId, + aRecord.audioControl.upLink, + aRecord.audioControl.downLink); + return; + } + }); + }, + + // nsIObserver + + observe: function(subject, topic, data) { + switch (topic) { + case kMozSettingsChangedObserverTopic: + if ("wrappedJSObject" in subject) { + subject = subject.wrappedJSObject; + } + this.handleSettingsChange(subject.key, subject.value, subject.isInternalChange); + break; + case kSysClockChangeObserverTopic: + let offset = parseInt(data, 10); + if (this._lastNitzMessage) { + this._lastNitzMessage.receiveTimeInMS += offset; + } + this._sntp.updateOffset(offset); + break; + case kNetworkConnStateChangedTopic: + let networkInfo = subject.QueryInterface(Ci.nsINetworkInfo); + if (networkInfo.state != Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + return; + } + + // SNTP can only update when we have mobile or Wifi connections. + if (networkInfo.type != NETWORK_TYPE_WIFI && + networkInfo.type != NETWORK_TYPE_MOBILE) { + return; + } + + // If the network comes from RIL, make sure the RIL service is matched. + if (subject instanceof Ci.nsIRilNetworkInfo) { + networkInfo = subject.QueryInterface(Ci.nsIRilNetworkInfo); + if (networkInfo.serviceId != this.clientId) { + return; + } + } + + // SNTP won't update unless the SNTP is already expired. + if (this._sntp.isExpired()) { + this._sntp.request(); + } + break; + case kScreenStateChangedTopic: + this.workerMessenger.send("setScreenState", { on: (data === "on") }); + break; + } + }, + + // Flag to determine whether to update system clock automatically. It + // corresponds to the "time.clock.automatic-update.enabled" setting. + _clockAutoUpdateEnabled: null, + + // Flag to determine whether to update system timezone automatically. It + // corresponds to the "time.clock.automatic-update.enabled" setting. + _timezoneAutoUpdateEnabled: null, + + // Remember the last NITZ message so that we can set the time based on + // the network immediately when users enable network-based time. + _lastNitzMessage: null, + + // Object that handles SNTP. + _sntp: null, + + // Cell Broadcast settings values. + _cellBroadcastSearchList: null, + + handleSettingsChange: function(aName, aResult, aIsInternalSetting) { + // Don't allow any content processes to modify the setting + // "time.clock.automatic-update.available" except for the chrome process. + if (aName === kSettingsClockAutoUpdateAvailable && + !aIsInternalSetting) { + let isClockAutoUpdateAvailable = this._lastNitzMessage !== null || + this._sntp.isAvailable(); + if (aResult !== isClockAutoUpdateAvailable) { + if (DEBUG) { + debug("Content processes cannot modify 'time.clock.automatic-update.available'. Restore!"); + } + // Restore the setting to the current value. + this.setClockAutoUpdateAvailable(isClockAutoUpdateAvailable); + } + } + + // Don't allow any content processes to modify the setting + // "time.timezone.automatic-update.available" except for the chrome + // process. + if (aName === kSettingsTimezoneAutoUpdateAvailable && + !aIsInternalSetting) { + let isTimezoneAutoUpdateAvailable = this._lastNitzMessage !== null; + if (aResult !== isTimezoneAutoUpdateAvailable) { + if (DEBUG) { + this.debug("Content processes cannot modify 'time.timezone.automatic-update.available'. Restore!"); + } + // Restore the setting to the current value. + this.setTimezoneAutoUpdateAvailable(isTimezoneAutoUpdateAvailable); + } + } + + this.handle(aName, aResult); + }, + + // nsISettingsServiceCallback + handle: function(aName, aResult) { + switch(aName) { + case kSettingsClockAutoUpdateEnabled: + this._clockAutoUpdateEnabled = aResult; + if (!this._clockAutoUpdateEnabled) { + break; + } + + // Set the latest cached NITZ time if it's available. + if (this._lastNitzMessage) { + this.setClockByNitz(this._lastNitzMessage); + } else if (gNetworkManager.activeNetworkInfo && + gNetworkManager.activeNetworkInfo.state == + Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + // Set the latest cached SNTP time if it's available. + if (!this._sntp.isExpired()) { + this.setClockBySntp(this._sntp.getOffset()); + } else { + // Or refresh the SNTP. + this._sntp.request(); + } + } else { + // Set a sane minimum time. + let buildTime = libcutils.property_get("ro.build.date.utc", "0") * 1000; + let file = FileUtils.File("/system/b2g/b2g"); + if (file.lastModifiedTime > buildTime) { + buildTime = file.lastModifiedTime; + } + if (buildTime > Date.now()) { + gTimeService.set(buildTime); + } + } + break; + case kSettingsTimezoneAutoUpdateEnabled: + this._timezoneAutoUpdateEnabled = aResult; + + if (this._timezoneAutoUpdateEnabled) { + // Apply the latest cached NITZ for timezone if it's available. + if (this._timezoneAutoUpdateEnabled && this._lastNitzMessage) { + this.setTimezoneByNitz(this._lastNitzMessage); + } + } + break; + } + }, + + handleError: function(aErrorMessage) { + if (DEBUG) { + this.debug("There was an error while reading RIL settings."); + } + }, + + // nsIRadioInterface + + // TODO: Bug 928861 - B2G NetworkManager: Provide a more generic function + // for connecting + setupDataCallByType: function(networkType) { + let connHandler = gDataCallManager.getDataCallHandler(this.clientId); + connHandler.setupDataCallByType(networkType); + }, + + // TODO: Bug 928861 - B2G NetworkManager: Provide a more generic function + // for connecting + deactivateDataCallByType: function(networkType) { + let connHandler = gDataCallManager.getDataCallHandler(this.clientId); + connHandler.deactivateDataCallByType(networkType); + }, + + // TODO: Bug 904514 - [meta] NetworkManager enhancement + getDataCallStateByType: function(networkType) { + let connHandler = gDataCallManager.getDataCallHandler(this.clientId); + return connHandler.getDataCallStateByType(networkType); + }, + + sendWorkerMessage: function(rilMessageType, message, callback) { + // Special handler for setRadioEnabled. + if (rilMessageType === "setRadioEnabled") { + // Forward it to gRadioEnabledController. + gRadioEnabledController.setRadioEnabled(this.clientId, message, + callback.handleResponse); + return; + } + + if (callback) { + this.workerMessenger.send(rilMessageType, message, function(response) { + return callback.handleResponse(response); + }); + } else { + this.workerMessenger.send(rilMessageType, message); + } + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RadioInterfaceLayer]); diff --git a/dom/system/gonk/RadioInterfaceLayer.manifest b/dom/system/gonk/RadioInterfaceLayer.manifest new file mode 100644 index 000000000..3c7c3b808 --- /dev/null +++ b/dom/system/gonk/RadioInterfaceLayer.manifest @@ -0,0 +1,18 @@ +# Copyright 2012 Mozilla Foundation and Mozilla contributors +# +# 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. + +# RadioInterfaceLayer.js +component {2d831c8d-6017-435b-a80c-e5d422810cea} RadioInterfaceLayer.js +contract @mozilla.org/ril;1 {2d831c8d-6017-435b-a80c-e5d422810cea} +category profile-after-change RadioInterfaceLayer @mozilla.org/ril;1 diff --git a/dom/system/gonk/SystemProperty.cpp b/dom/system/gonk/SystemProperty.cpp new file mode 100644 index 000000000..1f874ce90 --- /dev/null +++ b/dom/system/gonk/SystemProperty.cpp @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SystemProperty.h" + +#include <dlfcn.h> +#include <string.h> + +#include "nsDebug.h" +#include "prinit.h" + +namespace mozilla { +namespace system { + +namespace { + +typedef int (*PropertyGet)(const char*, char*, const char*); +typedef int (*PropertySet)(const char*, const char*); + +static void *sLibcUtils; +static PRCallOnceType sInitLibcUtils; + +static int +FakePropertyGet(const char* key, char* value, const char* default_value) +{ + if(!default_value) { + value[0] = '\0'; + return 0; + } + + int len = strlen(default_value); + if (len >= Property::VALUE_MAX_LENGTH) { + len = Property::VALUE_MAX_LENGTH - 1; + } + memcpy(value, default_value, len); + value[len] = '\0'; + + return len; +} + +static int +FakePropertySet(const char* key, const char* value) +{ + return 0; +} + +static PRStatus +InitLibcUtils() +{ + sLibcUtils = dlopen("/system/lib/libcutils.so", RTLD_LAZY); + // We will fallback to the fake getter/setter when sLibcUtils is not valid. + return PR_SUCCESS; +} + +static void* +GetLibcUtils() +{ + PR_CallOnce(&sInitLibcUtils, InitLibcUtils); + return sLibcUtils; +} + +} // anonymous namespace + +/*static*/ int +Property::Get(const char* key, char* value, const char* default_value) +{ + void *libcutils = GetLibcUtils(); + if (libcutils) { + PropertyGet getter = (PropertyGet) dlsym(libcutils, "property_get"); + if (getter) { + return getter(key, value, default_value); + } + NS_WARNING("Failed to get property_get() from libcutils!"); + } + NS_WARNING("Fallback to the FakePropertyGet()"); + return FakePropertyGet(key, value, default_value); +} + +/*static*/ int +Property::Set(const char* key, const char* value) +{ + void *libcutils = GetLibcUtils(); + if (libcutils) { + PropertySet setter = (PropertySet) dlsym(libcutils, "property_set"); + if (setter) { + return setter(key, value); + } + NS_WARNING("Failed to get property_set() from libcutils!"); + } + NS_WARNING("Fallback to the FakePropertySet()"); + return FakePropertySet(key, value); +} + +} // namespace system +} // namespace mozilla diff --git a/dom/system/gonk/SystemProperty.h b/dom/system/gonk/SystemProperty.h new file mode 100644 index 000000000..2b5ceae8b --- /dev/null +++ b/dom/system/gonk/SystemProperty.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_Property_h +#define mozilla_system_Property_h + +namespace mozilla { +namespace system { + +/** +* Abstraction of property_get/property_get in libcutils from AOSP. +*/ +class Property +{ +public: + // Constants defined in system_properties.h from AOSP. + enum { + KEY_MAX_LENGTH = 32, + VALUE_MAX_LENGTH = 92 + }; + + static int + Get(const char* key, char* value, const char* default_value); + + static int + Set(const char* key, const char* value); + +private: + Property() {} + virtual ~Property() {} +}; + +} // namespace system +} // namespace mozilla + + #endif // mozilla_system_Property_h diff --git a/dom/system/gonk/SystemWorkerManager.cpp b/dom/system/gonk/SystemWorkerManager.cpp new file mode 100644 index 000000000..ee3fc8de3 --- /dev/null +++ b/dom/system/gonk/SystemWorkerManager.cpp @@ -0,0 +1,214 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +#include "SystemWorkerManager.h" + +#include "nsINetworkService.h" +#include "nsIWifi.h" +#include "nsIWorkerHolder.h" +#include "nsIXPConnect.h" + +#include "jsfriendapi.h" +#include "mozilla/dom/workers/Workers.h" +#include "AutoMounter.h" +#include "TimeZoneSettingObserver.h" +#include "AudioManager.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/ipc/KeyStore.h" +#include "nsIObserverService.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "WifiWorker.h" +#include "mozilla/Services.h" + +USING_WORKERS_NAMESPACE + +using namespace mozilla::dom::gonk; +using namespace mozilla::ipc; +using namespace mozilla::system; + +namespace { + +NS_DEFINE_CID(kWifiWorkerCID, NS_WIFIWORKER_CID); + +// Doesn't carry a reference, we're owned by services. +SystemWorkerManager *gInstance = nullptr; + +} // namespace + +SystemWorkerManager::SystemWorkerManager() + : mShutdown(false) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!gInstance, "There should only be one instance!"); +} + +SystemWorkerManager::~SystemWorkerManager() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + NS_ASSERTION(!gInstance || gInstance == this, + "There should only be one instance!"); + gInstance = nullptr; +} + +nsresult +SystemWorkerManager::Init() +{ + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + NS_ASSERTION(NS_IsMainThread(), "We can only initialize on the main thread"); + NS_ASSERTION(!mShutdown, "Already shutdown!"); + + nsresult rv = InitWifi(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to initialize WiFi Networking!"); + return rv; + } + + InitKeyStore(); + + InitAutoMounter(); + InitializeTimeZoneSettingObserver(); + nsCOMPtr<nsIAudioManager> audioManager = + do_GetService(NS_AUDIOMANAGER_CONTRACTID); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) { + NS_WARNING("Failed to get observer service!"); + return NS_ERROR_FAILURE; + } + + rv = obs->AddObserver(this, WORKERS_SHUTDOWN_TOPIC, false); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to initialize worker shutdown event!"); + return rv; + } + + return NS_OK; +} + +void +SystemWorkerManager::Shutdown() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + mShutdown = true; + + ShutdownAutoMounter(); + + nsCOMPtr<nsIWifi> wifi(do_QueryInterface(mWifiWorker)); + if (wifi) { + wifi->Shutdown(); + wifi = nullptr; + } + mWifiWorker = nullptr; + + if (mKeyStore) { + mKeyStore->Shutdown(); + mKeyStore = nullptr; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, WORKERS_SHUTDOWN_TOPIC); + } +} + +// static +already_AddRefed<SystemWorkerManager> +SystemWorkerManager::FactoryCreate() +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + RefPtr<SystemWorkerManager> instance(gInstance); + + if (!instance) { + instance = new SystemWorkerManager(); + if (NS_FAILED(instance->Init())) { + instance->Shutdown(); + return nullptr; + } + + gInstance = instance; + } + + return instance.forget(); +} + +// static +nsIInterfaceRequestor* +SystemWorkerManager::GetInterfaceRequestor() +{ + return gInstance; +} + +NS_IMETHODIMP +SystemWorkerManager::GetInterface(const nsIID &aIID, void **aResult) +{ + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + + if (aIID.Equals(NS_GET_IID(nsIWifi))) { + return CallQueryInterface(mWifiWorker, + reinterpret_cast<nsIWifi**>(aResult)); + } + + NS_WARNING("Got nothing for the requested IID!"); + return NS_ERROR_NO_INTERFACE; +} + +nsresult +SystemWorkerManager::RegisterRilWorker(unsigned int aClientId, + JS::Handle<JS::Value> aWorker, + JSContext *aCx) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult +SystemWorkerManager::InitWifi() +{ + nsCOMPtr<nsIWorkerHolder> worker = do_CreateInstance(kWifiWorkerCID); + NS_ENSURE_TRUE(worker, NS_ERROR_FAILURE); + + mWifiWorker = worker; + return NS_OK; +} + +nsresult +SystemWorkerManager::InitKeyStore() +{ + mKeyStore = new KeyStore(); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(SystemWorkerManager, + nsIObserver, + nsIInterfaceRequestor, + nsISystemWorkerManager) + +NS_IMETHODIMP +SystemWorkerManager::Observe(nsISupports *aSubject, const char *aTopic, + const char16_t *aData) +{ + if (!strcmp(aTopic, WORKERS_SHUTDOWN_TOPIC)) { + Shutdown(); + } + + return NS_OK; +} diff --git a/dom/system/gonk/SystemWorkerManager.h b/dom/system/gonk/SystemWorkerManager.h new file mode 100644 index 000000000..625cda261 --- /dev/null +++ b/dom/system/gonk/SystemWorkerManager.h @@ -0,0 +1,75 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=40: */ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +#ifndef mozilla_dom_system_b2g_systemworkermanager_h__ +#define mozilla_dom_system_b2g_systemworkermanager_h__ + +#include "nsIInterfaceRequestor.h" +#include "nsISystemWorkerManager.h" +#include "nsIObserver.h" +#include "nsCOMPtr.h" +#include "nsXULAppAPI.h" // For XRE_GetProcessType + +class nsIWorkerHolder; + +namespace mozilla { + +namespace ipc { + class KeyStore; +} + +namespace dom { +namespace gonk { + +class SystemWorkerManager final : public nsIObserver, + public nsIInterfaceRequestor, + public nsISystemWorkerManager +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSISYSTEMWORKERMANAGER + + nsresult Init(); + void Shutdown(); + + static already_AddRefed<SystemWorkerManager> + FactoryCreate(); + + static nsIInterfaceRequestor* + GetInterfaceRequestor(); + +private: + SystemWorkerManager(); + ~SystemWorkerManager(); + + nsresult InitWifi(); + nsresult InitKeyStore(); + + nsCOMPtr<nsIWorkerHolder> mWifiWorker; + + RefPtr<mozilla::ipc::KeyStore> mKeyStore; + + bool mShutdown; +}; + +} +} +} + +#endif // mozilla_dom_system_b2g_systemworkermanager_h__ diff --git a/dom/system/gonk/TetheringService.js b/dom/system/gonk/TetheringService.js new file mode 100644 index 000000000..c5c478180 --- /dev/null +++ b/dom/system/gonk/TetheringService.js @@ -0,0 +1,891 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/systemlibs.js"); + +const TETHERINGSERVICE_CONTRACTID = "@mozilla.org/tethering/service;1"; +const TETHERINGSERVICE_CID = + Components.ID("{527a4121-ee5a-4651-be9c-f46f59cf7c01}"); + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkManager", + "@mozilla.org/network/manager;1", + "nsINetworkManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "gNetworkService", + "@mozilla.org/network/service;1", + "nsINetworkService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +XPCOMUtils.defineLazyServiceGetter(this, "gMobileConnectionService", + "@mozilla.org/mobileconnection/mobileconnectionservice;1", + "nsIMobileConnectionService"); + +XPCOMUtils.defineLazyGetter(this, "gRil", function() { + try { + return Cc["@mozilla.org/ril;1"].getService(Ci.nsIRadioInterfaceLayer); + } catch (e) {} + + return null; +}); + +const TOPIC_MOZSETTINGS_CHANGED = "mozsettings-changed"; +const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed"; +const TOPIC_PREF_CHANGED = "nsPref:changed"; +const TOPIC_XPCOM_SHUTDOWN = "xpcom-shutdown"; +const PREF_MANAGE_OFFLINE_STATUS = "network.gonk.manage-offline-status"; +const PREF_NETWORK_DEBUG_ENABLED = "network.debugging.enabled"; + +const POSSIBLE_USB_INTERFACE_NAME = "rndis0,usb0"; +const DEFAULT_USB_INTERFACE_NAME = "rndis0"; +const DEFAULT_3G_INTERFACE_NAME = "rmnet0"; +const DEFAULT_WIFI_INTERFACE_NAME = "wlan0"; + +// The kernel's proc entry for network lists. +const KERNEL_NETWORK_ENTRY = "/sys/class/net"; + +const TETHERING_TYPE_WIFI = "WiFi"; +const TETHERING_TYPE_USB = "USB"; + +const WIFI_FIRMWARE_AP = "AP"; +const WIFI_FIRMWARE_STATION = "STA"; +const WIFI_SECURITY_TYPE_NONE = "open"; +const WIFI_SECURITY_TYPE_WPA_PSK = "wpa-psk"; +const WIFI_SECURITY_TYPE_WPA2_PSK = "wpa2-psk"; +const WIFI_CTRL_INTERFACE = "wl0.1"; + +const NETWORK_INTERFACE_UP = "up"; +const NETWORK_INTERFACE_DOWN = "down"; + +const TETHERING_STATE_ONGOING = "ongoing"; +const TETHERING_STATE_IDLE = "idle"; +const TETHERING_STATE_ACTIVE = "active"; + +// Settings DB path for USB tethering. +const SETTINGS_USB_ENABLED = "tethering.usb.enabled"; +const SETTINGS_USB_IP = "tethering.usb.ip"; +const SETTINGS_USB_PREFIX = "tethering.usb.prefix"; +const SETTINGS_USB_DHCPSERVER_STARTIP = "tethering.usb.dhcpserver.startip"; +const SETTINGS_USB_DHCPSERVER_ENDIP = "tethering.usb.dhcpserver.endip"; +const SETTINGS_USB_DNS1 = "tethering.usb.dns1"; +const SETTINGS_USB_DNS2 = "tethering.usb.dns2"; + +// Settings DB path for WIFI tethering. +const SETTINGS_WIFI_DHCPSERVER_STARTIP = "tethering.wifi.dhcpserver.startip"; +const SETTINGS_WIFI_DHCPSERVER_ENDIP = "tethering.wifi.dhcpserver.endip"; + +// Settings DB patch for dun required setting. +const SETTINGS_DUN_REQUIRED = "tethering.dun.required"; + +// Default value for USB tethering. +const DEFAULT_USB_IP = "192.168.0.1"; +const DEFAULT_USB_PREFIX = "24"; +const DEFAULT_USB_DHCPSERVER_STARTIP = "192.168.0.10"; +const DEFAULT_USB_DHCPSERVER_ENDIP = "192.168.0.30"; + +const DEFAULT_DNS1 = "8.8.8.8"; +const DEFAULT_DNS2 = "8.8.4.4"; + +const DEFAULT_WIFI_DHCPSERVER_STARTIP = "192.168.1.10"; +const DEFAULT_WIFI_DHCPSERVER_ENDIP = "192.168.1.30"; + +const SETTINGS_DATA_DEFAULT_SERVICE_ID = "ril.data.defaultServiceId"; +const MOBILE_DUN_CONNECT_TIMEOUT = 30000; +const MOBILE_DUN_RETRY_INTERVAL = 5000; +const MOBILE_DUN_MAX_RETRIES = 5; + +var debug; +function updateDebug() { + let debugPref = false; // set default value here. + try { + debugPref = debugPref || Services.prefs.getBoolPref(PREF_NETWORK_DEBUG_ENABLED); + } catch (e) {} + + if (debugPref) { + debug = function(s) { + dump("-*- TetheringService: " + s + "\n"); + }; + } else { + debug = function(s) {}; + } +} +updateDebug(); + +function TetheringService() { + Services.obs.addObserver(this, TOPIC_XPCOM_SHUTDOWN, false); + Services.obs.addObserver(this, TOPIC_MOZSETTINGS_CHANGED, false); + Services.obs.addObserver(this, TOPIC_CONNECTION_STATE_CHANGED, false); + Services.prefs.addObserver(PREF_NETWORK_DEBUG_ENABLED, this, false); + Services.prefs.addObserver(PREF_MANAGE_OFFLINE_STATUS, this, false); + + try { + this._manageOfflineStatus = + Services.prefs.getBoolPref(PREF_MANAGE_OFFLINE_STATUS); + } catch(ex) { + // Ignore. + } + + this._dataDefaultServiceId = 0; + + // Possible usb tethering interfaces for different gonk platform. + this.possibleInterface = POSSIBLE_USB_INTERFACE_NAME.split(","); + + // Default values for internal and external interfaces. + this._tetheringInterface = {}; + this._tetheringInterface[TETHERING_TYPE_USB] = { + externalInterface: DEFAULT_3G_INTERFACE_NAME, + internalInterface: DEFAULT_USB_INTERFACE_NAME + }; + this._tetheringInterface[TETHERING_TYPE_WIFI] = { + externalInterface: DEFAULT_3G_INTERFACE_NAME, + internalInterface: DEFAULT_WIFI_INTERFACE_NAME + }; + + this.tetheringSettings = {}; + this.initTetheringSettings(); + + let settingsLock = gSettingsService.createLock(); + // Read the default service id for data call. + settingsLock.get(SETTINGS_DATA_DEFAULT_SERVICE_ID, this); + + // Read usb tethering data from settings DB. + settingsLock.get(SETTINGS_USB_IP, this); + settingsLock.get(SETTINGS_USB_PREFIX, this); + settingsLock.get(SETTINGS_USB_DHCPSERVER_STARTIP, this); + settingsLock.get(SETTINGS_USB_DHCPSERVER_ENDIP, this); + settingsLock.get(SETTINGS_USB_DNS1, this); + settingsLock.get(SETTINGS_USB_DNS2, this); + settingsLock.get(SETTINGS_USB_ENABLED, this); + + // Read wifi tethering data from settings DB. + settingsLock.get(SETTINGS_WIFI_DHCPSERVER_STARTIP, this); + settingsLock.get(SETTINGS_WIFI_DHCPSERVER_ENDIP, this); + + this._usbTetheringSettingsToRead = [SETTINGS_USB_IP, + SETTINGS_USB_PREFIX, + SETTINGS_USB_DHCPSERVER_STARTIP, + SETTINGS_USB_DHCPSERVER_ENDIP, + SETTINGS_USB_DNS1, + SETTINGS_USB_DNS2, + SETTINGS_USB_ENABLED, + SETTINGS_WIFI_DHCPSERVER_STARTIP, + SETTINGS_WIFI_DHCPSERVER_ENDIP]; + + this.wantConnectionEvent = null; + + this.dunConnectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + this.dunRetryTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + + this._pendingTetheringRequests = []; +} +TetheringService.prototype = { + classID: TETHERINGSERVICE_CID, + classInfo: XPCOMUtils.generateCI({classID: TETHERINGSERVICE_CID, + contractID: TETHERINGSERVICE_CONTRACTID, + classDescription: "Tethering Service", + interfaces: [Ci.nsITetheringService]}), + QueryInterface: XPCOMUtils.generateQI([Ci.nsITetheringService, + Ci.nsISupportsWeakReference, + Ci.nsIObserver, + Ci.nsISettingsServiceCallback]), + + // Flag to record the default client id for data call. + _dataDefaultServiceId: null, + + // Number of usb tehering requests to be processed. + _usbTetheringRequestCount: 0, + + // Usb tethering state. + _usbTetheringAction: TETHERING_STATE_IDLE, + + // Tethering settings. + tetheringSettings: null, + + // Tethering settings need to be read from settings DB. + _usbTetheringSettingsToRead: null, + + // Previous usb tethering enabled state. + _oldUsbTetheringEnabledState: null, + + // External and internal interface name. + _tetheringInterface: null, + + // Dun connection timer. + dunConnectTimer: null, + + // Dun connection retry times. + dunRetryTimes: 0, + + // Dun retry timer. + dunRetryTimer: null, + + // Pending tethering request to handle after dun is connected. + _pendingTetheringRequests: null, + + // Flag to indicate wether wifi tethering is being processed. + _wifiTetheringRequestOngoing: false, + + // Arguments for pending wifi tethering request. + _pendingWifiTetheringRequestArgs: null, + + // The state of tethering. + state: Ci.nsITetheringService.TETHERING_STATE_INACTIVE, + + // Flag to check if we can modify the Services.io.offline. + _manageOfflineStatus: true, + + // nsIObserver + + observe: function(aSubject, aTopic, aData) { + let network; + + switch(aTopic) { + case TOPIC_PREF_CHANGED: + if (aData === PREF_NETWORK_DEBUG_ENABLED) { + updateDebug(); + } + break; + case TOPIC_MOZSETTINGS_CHANGED: + if ("wrappedJSObject" in aSubject) { + aSubject = aSubject.wrappedJSObject; + } + this.handle(aSubject.key, aSubject.value); + break; + case TOPIC_CONNECTION_STATE_CHANGED: + network = aSubject.QueryInterface(Ci.nsINetworkInfo); + debug("Network " + network.type + "/" + network.name + + " changed state to " + network.state); + this.onConnectionChanged(network); + break; + case TOPIC_XPCOM_SHUTDOWN: + Services.obs.removeObserver(this, TOPIC_XPCOM_SHUTDOWN); + Services.obs.removeObserver(this, TOPIC_MOZSETTINGS_CHANGED); + Services.obs.removeObserver(this, TOPIC_CONNECTION_STATE_CHANGED); + Services.prefs.removeObserver(PREF_NETWORK_DEBUG_ENABLED, this); + Services.prefs.removeObserver(PREF_MANAGE_OFFLINE_STATUS, this); + + this.dunConnectTimer.cancel(); + this.dunRetryTimer.cancel(); + break; + case PREF_MANAGE_OFFLINE_STATUS: + try { + this._manageOfflineStatus = + Services.prefs.getBoolPref(PREF_MANAGE_OFFLINE_STATUS); + } catch(ex) { + // Ignore. + } + break; + } + }, + + // nsISettingsServiceCallback + + handle: function(aName, aResult) { + switch(aName) { + case SETTINGS_DATA_DEFAULT_SERVICE_ID: + this._dataDefaultServiceId = aResult || 0; + debug("'_dataDefaultServiceId' is now " + this._dataDefaultServiceId); + break; + case SETTINGS_USB_ENABLED: + this._oldUsbTetheringEnabledState = this.tetheringSettings[SETTINGS_USB_ENABLED]; + case SETTINGS_USB_IP: + case SETTINGS_USB_PREFIX: + case SETTINGS_USB_DHCPSERVER_STARTIP: + case SETTINGS_USB_DHCPSERVER_ENDIP: + case SETTINGS_USB_DNS1: + case SETTINGS_USB_DNS2: + case SETTINGS_WIFI_DHCPSERVER_STARTIP: + case SETTINGS_WIFI_DHCPSERVER_ENDIP: + if (aResult !== null) { + this.tetheringSettings[aName] = aResult; + } + debug("'" + aName + "'" + " is now " + this.tetheringSettings[aName]); + let index = this._usbTetheringSettingsToRead.indexOf(aName); + + if (index != -1) { + this._usbTetheringSettingsToRead.splice(index, 1); + } + + if (this._usbTetheringSettingsToRead.length) { + debug("We haven't read completely the usb Tethering data from settings db."); + break; + } + + if (this._oldUsbTetheringEnabledState === this.tetheringSettings[SETTINGS_USB_ENABLED]) { + debug("No changes for SETTINGS_USB_ENABLED flag. Nothing to do."); + this.handlePendingWifiTetheringRequest(); + break; + } + + this._usbTetheringRequestCount++; + if (this._usbTetheringRequestCount === 1) { + if (this._wifiTetheringRequestOngoing) { + debug('USB tethering request is blocked by ongoing wifi tethering request.'); + } else { + this.handleLastUsbTetheringRequest(); + } + } + break; + }; + }, + + handleError: function(aErrorMessage) { + debug("There was an error while reading Tethering settings."); + this.tetheringSettings = {}; + this.tetheringSettings[SETTINGS_USB_ENABLED] = false; + }, + + initTetheringSettings: function() { + this.tetheringSettings[SETTINGS_USB_ENABLED] = false; + this.tetheringSettings[SETTINGS_USB_IP] = DEFAULT_USB_IP; + this.tetheringSettings[SETTINGS_USB_PREFIX] = DEFAULT_USB_PREFIX; + this.tetheringSettings[SETTINGS_USB_DHCPSERVER_STARTIP] = DEFAULT_USB_DHCPSERVER_STARTIP; + this.tetheringSettings[SETTINGS_USB_DHCPSERVER_ENDIP] = DEFAULT_USB_DHCPSERVER_ENDIP; + this.tetheringSettings[SETTINGS_USB_DNS1] = DEFAULT_DNS1; + this.tetheringSettings[SETTINGS_USB_DNS2] = DEFAULT_DNS2; + + this.tetheringSettings[SETTINGS_WIFI_DHCPSERVER_STARTIP] = DEFAULT_WIFI_DHCPSERVER_STARTIP; + this.tetheringSettings[SETTINGS_WIFI_DHCPSERVER_ENDIP] = DEFAULT_WIFI_DHCPSERVER_ENDIP; + + this.tetheringSettings[SETTINGS_DUN_REQUIRED] = + libcutils.property_get("ro.tethering.dun_required") === "1"; + }, + + getNetworkInfo: function(aType, aServiceId) { + for (let networkId in gNetworkManager.allNetworkInfo) { + let networkInfo = gNetworkManager.allNetworkInfo[networkId]; + if (networkInfo.type == aType) { + try { + if (networkInfo instanceof Ci.nsIRilNetworkInfo) { + let rilNetwork = networkInfo.QueryInterface(Ci.nsIRilNetworkInfo); + if (rilNetwork.serviceId != aServiceId) { + continue; + } + } + } catch (e) {} + return networkInfo; + } + } + return null; + }, + + handleLastUsbTetheringRequest: function() { + debug('handleLastUsbTetheringRequest... ' + this._usbTetheringRequestCount); + + if (this._usbTetheringRequestCount === 0) { + if (this.wantConnectionEvent) { + if (this.tetheringSettings[SETTINGS_USB_ENABLED]) { + this.wantConnectionEvent.call(this); + } + this.wantConnectionEvent = null; + } + this.handlePendingWifiTetheringRequest(); + return; + } + + // Cancel the accumlated count to 1 since we only care about the + // last state. + this._usbTetheringRequestCount = 1; + this.handleUSBTetheringToggle(this.tetheringSettings[SETTINGS_USB_ENABLED]); + this.wantConnectionEvent = null; + }, + + handlePendingWifiTetheringRequest: function() { + if (this._pendingWifiTetheringRequestArgs) { + this.setWifiTethering.apply(this, this._pendingWifiTetheringRequestArgs); + this._pendingWifiTetheringRequestArgs = null; + } + }, + + /** + * Callback when dun connection fails to connect within timeout. + */ + onDunConnectTimerTimeout: function() { + while (this._pendingTetheringRequests.length > 0) { + debug("onDunConnectTimerTimeout: callback without network info."); + let callback = this._pendingTetheringRequests.shift(); + if (typeof callback === 'function') { + callback(); + } + } + }, + + setupDunConnection: function() { + this.dunRetryTimer.cancel(); + let connection = + gMobileConnectionService.getItemByServiceId(this._dataDefaultServiceId); + let data = connection && connection.data; + if (data && data.state === "registered") { + let ril = gRil.getRadioInterface(this._dataDefaultServiceId); + + this.dunRetryTimes = 0; + ril.setupDataCallByType(Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN); + this.dunConnectTimer.cancel(); + this.dunConnectTimer. + initWithCallback(this.onDunConnectTimerTimeout.bind(this), + MOBILE_DUN_CONNECT_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + + if (this.dunRetryTimes++ >= this.MOBILE_DUN_MAX_RETRIES) { + debug("setupDunConnection: max retries reached."); + this.dunRetryTimes = 0; + // same as dun connect timeout. + this.onDunConnectTimerTimeout(); + return; + } + + debug("Data not ready, retry dun after " + MOBILE_DUN_RETRY_INTERVAL + " ms."); + this.dunRetryTimer. + initWithCallback(this.setupDunConnection.bind(this), + MOBILE_DUN_RETRY_INTERVAL, Ci.nsITimer.TYPE_ONE_SHOT); + }, + + _dunActiveUsers: 0, + handleDunConnection: function(aEnable, aCallback) { + debug("handleDunConnection: " + aEnable); + let dun = this.getNetworkInfo( + Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN, this._dataDefaultServiceId); + + if (!aEnable) { + this._dunActiveUsers--; + if (this._dunActiveUsers > 0) { + debug("Dun still needed by others, do not disconnect.") + return; + } + + this.dunRetryTimes = 0; + this.dunRetryTimer.cancel(); + this.dunConnectTimer.cancel(); + this._pendingTetheringRequests = []; + + if (dun && (dun.state == Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED)) { + gRil.getRadioInterface(this._dataDefaultServiceId) + .deactivateDataCallByType(Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN); + } + return; + } + + this._dunActiveUsers++; + if (!dun || (dun.state != Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED)) { + debug("DUN data call inactive, setup dun data call!") + this._pendingTetheringRequests.push(aCallback); + this.dunRetryTimes = 0; + this.setupDunConnection(); + + return; + } + + this._tetheringInterface[TETHERING_TYPE_USB].externalInterface = dun.name; + aCallback(dun); + }, + + handleUSBTetheringToggle: function(aEnable) { + debug("handleUSBTetheringToggle: " + aEnable); + if (aEnable && + (this._usbTetheringAction === TETHERING_STATE_ONGOING || + this._usbTetheringAction === TETHERING_STATE_ACTIVE)) { + debug("Usb tethering already connecting/connected."); + this._usbTetheringRequestCount = 0; + this.handlePendingWifiTetheringRequest(); + return; + } + + if (!aEnable && + this._usbTetheringAction === TETHERING_STATE_IDLE) { + debug("Usb tethering already disconnected."); + this._usbTetheringRequestCount = 0; + this.handlePendingWifiTetheringRequest(); + return; + } + + if (!aEnable) { + this.tetheringSettings[SETTINGS_USB_ENABLED] = false; + gNetworkService.enableUsbRndis(false, this.enableUsbRndisResult.bind(this)); + return; + } + + this.tetheringSettings[SETTINGS_USB_ENABLED] = true; + this._usbTetheringAction = TETHERING_STATE_ONGOING; + + if (this.tetheringSettings[SETTINGS_DUN_REQUIRED]) { + this.handleDunConnection(true, (aNetworkInfo) => { + if (!aNetworkInfo){ + this.usbTetheringResultReport(aEnable, "Dun connection failed"); + return; + } + this._tetheringInterface[TETHERING_TYPE_USB].externalInterface = + aNetworkInfo.name; + gNetworkService.enableUsbRndis(true, this.enableUsbRndisResult.bind(this)); + }); + return; + } + + if (gNetworkManager.activeNetworkInfo) { + this._tetheringInterface[TETHERING_TYPE_USB].externalInterface = + gNetworkManager.activeNetworkInfo.name; + } else { + let mobile = this.getNetworkInfo( + Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE, this._dataDefaultServiceId); + if (mobile && mobile.name) { + this._tetheringInterface[TETHERING_TYPE_USB].externalInterface = mobile.name; + } + } + gNetworkService.enableUsbRndis(true, this.enableUsbRndisResult.bind(this)); + }, + + getUSBTetheringParameters: function(aEnable, aTetheringInterface) { + let interfaceIp = this.tetheringSettings[SETTINGS_USB_IP]; + let prefix = this.tetheringSettings[SETTINGS_USB_PREFIX]; + let wifiDhcpStartIp = this.tetheringSettings[SETTINGS_WIFI_DHCPSERVER_STARTIP]; + let wifiDhcpEndIp = this.tetheringSettings[SETTINGS_WIFI_DHCPSERVER_ENDIP]; + let usbDhcpStartIp = this.tetheringSettings[SETTINGS_USB_DHCPSERVER_STARTIP]; + let usbDhcpEndIp = this.tetheringSettings[SETTINGS_USB_DHCPSERVER_ENDIP]; + let dns1 = this.tetheringSettings[SETTINGS_USB_DNS1]; + let dns2 = this.tetheringSettings[SETTINGS_USB_DNS2]; + let internalInterface = aTetheringInterface.internalInterface; + let externalInterface = aTetheringInterface.externalInterface; + + // Using the default values here until application support these settings. + if (interfaceIp == "" || prefix == "" || + wifiDhcpStartIp == "" || wifiDhcpEndIp == "" || + usbDhcpStartIp == "" || usbDhcpEndIp == "") { + debug("Invalid subnet information."); + return null; + } + + return { + ifname: internalInterface, + ip: interfaceIp, + prefix: prefix, + wifiStartIp: wifiDhcpStartIp, + wifiEndIp: wifiDhcpEndIp, + usbStartIp: usbDhcpStartIp, + usbEndIp: usbDhcpEndIp, + dns1: dns1, + dns2: dns2, + internalIfname: internalInterface, + externalIfname: externalInterface, + enable: aEnable, + link: aEnable ? NETWORK_INTERFACE_UP : NETWORK_INTERFACE_DOWN + }; + }, + + notifyError: function(aResetSettings, aCallback, aMsg) { + if (aResetSettings) { + let settingsLock = gSettingsService.createLock(); + // Disable wifi tethering with a useful error message for the user. + settingsLock.set("tethering.wifi.enabled", false, null, aMsg); + } + + debug("setWifiTethering: " + (aMsg ? aMsg : "success")); + + if (aCallback) { + // Callback asynchronously to avoid netsted toggling. + Services.tm.currentThread.dispatch(() => { + aCallback.wifiTetheringEnabledChange(aMsg); + }, Ci.nsIThread.DISPATCH_NORMAL); + } + }, + + enableWifiTethering: function(aEnable, aConfig, aCallback) { + // Fill in config's required fields. + aConfig.ifname = this._tetheringInterface[TETHERING_TYPE_WIFI].internalInterface; + aConfig.internalIfname = this._tetheringInterface[TETHERING_TYPE_WIFI].internalInterface; + aConfig.externalIfname = this._tetheringInterface[TETHERING_TYPE_WIFI].externalInterface; + + this._wifiTetheringRequestOngoing = true; + gNetworkService.setWifiTethering(aEnable, aConfig, (aError) => { + // Change the tethering state to WIFI if there is no error. + if (aEnable && !aError) { + this.state = Ci.nsITetheringService.TETHERING_STATE_WIFI; + } else { + // If wifi thethering is disable, or any error happens, + // then consider the following statements. + + // Check whether the state is USB now or not. If no then just change + // it to INACTIVE, if yes then just keep it. + // It means that don't let the disable or error of WIFI affect + // the original active state. + if (this.state != Ci.nsITetheringService.TETHERING_STATE_USB) { + this.state = Ci.nsITetheringService.TETHERING_STATE_INACTIVE; + } + + // Disconnect dun on error or when wifi tethering is disabled. + if (this.tetheringSettings[SETTINGS_DUN_REQUIRED]) { + this.handleDunConnection(false); + } + } + + if (this._manageOfflineStatus) { + Services.io.offline = !this.isAnyConnected() && + (this.state === + Ci.nsITetheringService.TETHERING_STATE_INACTIVE); + } + + let resetSettings = aError; + debug('gNetworkService.setWifiTethering finished'); + this.notifyError(resetSettings, aCallback, aError); + this._wifiTetheringRequestOngoing = false; + if (this._usbTetheringRequestCount > 0) { + debug('Perform pending USB tethering requests.'); + this.handleLastUsbTetheringRequest(); + } + }); + }, + + // Enable/disable WiFi tethering by sending commands to netd. + setWifiTethering: function(aEnable, aInterfaceName, aConfig, aCallback) { + debug("setWifiTethering: " + aEnable); + if (!aInterfaceName) { + this.notifyError(true, aCallback, "invalid network interface name"); + return; + } + + if (!aConfig) { + this.notifyError(true, aCallback, "invalid configuration"); + return; + } + + if (this._usbTetheringRequestCount > 0) { + // If there's still pending usb tethering request, save + // the request params and redo |setWifiTethering| on + // usb tethering task complete. + debug('USB tethering request is being processed. Queue this wifi tethering request.'); + this._pendingWifiTetheringRequestArgs = Array.prototype.slice.call(arguments); + debug('Pending args: ' + JSON.stringify(this._pendingWifiTetheringRequestArgs)); + return; + } + + // Re-check again, test cases set this property later. + this.tetheringSettings[SETTINGS_DUN_REQUIRED] = + libcutils.property_get("ro.tethering.dun_required") === "1"; + + if (!aEnable) { + this.enableWifiTethering(false, aConfig, aCallback); + return; + } + + this._tetheringInterface[TETHERING_TYPE_WIFI].internalInterface = + aInterfaceName; + + if (this.tetheringSettings[SETTINGS_DUN_REQUIRED]) { + this.handleDunConnection(true, (aNetworkInfo) => { + if (!aNetworkInfo) { + this.notifyError(true, aCallback, "Dun connection failed"); + return; + } + this._tetheringInterface[TETHERING_TYPE_WIFI].externalInterface = + aNetworkInfo.name; + this.enableWifiTethering(true, aConfig, aCallback); + }); + return; + } + + let mobile = this.getNetworkInfo( + Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE, this._dataDefaultServiceId); + // Update the real interface name + if (mobile && mobile.name) { + this._tetheringInterface[TETHERING_TYPE_WIFI].externalInterface = mobile.name; + } + + this.enableWifiTethering(true, aConfig, aCallback); + }, + + // Enable/disable USB tethering by sending commands to netd. + setUSBTethering: function(aEnable, aTetheringInterface, aCallback) { + let params = this.getUSBTetheringParameters(aEnable, aTetheringInterface); + + if (params === null) { + gNetworkService.enableUsbRndis(false, function() { + this.usbTetheringResultReport(aEnable, "Invalid parameters"); + }); + return; + } + + gNetworkService.setUSBTethering(aEnable, params, aCallback); + }, + + getUsbInterface: function() { + // Find the rndis interface. + for (let i = 0; i < this.possibleInterface.length; i++) { + try { + let file = new FileUtils.File(KERNEL_NETWORK_ENTRY + "/" + + this.possibleInterface[i]); + if (file.exists()) { + return this.possibleInterface[i]; + } + } catch (e) { + debug("Not " + this.possibleInterface[i] + " interface."); + } + } + debug("Can't find rndis interface in possible lists."); + return DEFAULT_USB_INTERFACE_NAME; + }, + + enableUsbRndisResult: function(aSuccess, aEnable) { + if (aSuccess) { + // If enable is false, don't find usb interface cause it is already down, + // just use the internal interface in settings. + if (aEnable) { + this._tetheringInterface[TETHERING_TYPE_USB].internalInterface = + this.getUsbInterface(); + } + this.setUSBTethering(aEnable, + this._tetheringInterface[TETHERING_TYPE_USB], + this.usbTetheringResultReport.bind(this, aEnable)); + } else { + this.usbTetheringResultReport(aEnable, "enableUsbRndisResult failure"); + throw new Error("failed to set USB Function to adb"); + } + }, + + usbTetheringResultReport: function(aEnable, aError) { + this._usbTetheringRequestCount--; + + let settingsLock = gSettingsService.createLock(); + + debug('usbTetheringResultReport callback. enable: ' + aEnable + + ', error: ' + aError); + + // Disable tethering settings when fail to enable it. + if (aError) { + this.tetheringSettings[SETTINGS_USB_ENABLED] = false; + settingsLock.set("tethering.usb.enabled", false, null); + // Skip others request when we found an error. + this._usbTetheringRequestCount = 0; + this._usbTetheringAction = TETHERING_STATE_IDLE; + // If the thethering state is WIFI now, then just keep it, + // if not, just change the state to INACTIVE. + // It means that don't let the error of USB affect the original active state. + if (this.state != Ci.nsITetheringService.TETHERING_STATE_WIFI) { + this.state = Ci.nsITetheringService.TETHERING_STATE_INACTIVE; + } + if (this.tetheringSettings[SETTINGS_DUN_REQUIRED]) { + this.handleDunConnection(false); + } + } else { + if (aEnable) { + this._usbTetheringAction = TETHERING_STATE_ACTIVE; + this.state = Ci.nsITetheringService.TETHERING_STATE_USB; + } else { + this._usbTetheringAction = TETHERING_STATE_IDLE; + // If the state is now WIFI, don't let the disable of USB affect it. + if (this.state != Ci.nsITetheringService.TETHERING_STATE_WIFI) { + this.state = Ci.nsITetheringService.TETHERING_STATE_INACTIVE; + } + if (this.tetheringSettings[SETTINGS_DUN_REQUIRED]) { + this.handleDunConnection(false); + } + } + + if (this._manageOfflineStatus) { + Services.io.offline = !this.isAnyConnected() && + (this.state === + Ci.nsITetheringService.TETHERING_STATE_INACTIVE); + } + + this.handleLastUsbTetheringRequest(); + } + }, + + onConnectionChangedReport: function(aSuccess, aExternalIfname) { + debug("onConnectionChangedReport result: success " + aSuccess); + + if (aSuccess) { + // Update the external interface. + this._tetheringInterface[TETHERING_TYPE_USB].externalInterface = + aExternalIfname; + debug("Change the interface name to " + aExternalIfname); + } + }, + + onConnectionChanged: function(aNetworkInfo) { + if (aNetworkInfo.state != Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + debug("We are only interested in CONNECTED event"); + return; + } + + if (this.tetheringSettings[SETTINGS_DUN_REQUIRED] && + aNetworkInfo.type === Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN) { + this.dunConnectTimer.cancel(); + debug("DUN data call connected, process callbacks."); + while (this._pendingTetheringRequests.length > 0) { + let callback = this._pendingTetheringRequests.shift(); + if (typeof callback === 'function') { + callback(aNetworkInfo); + } + } + return; + } + + if (!this.tetheringSettings[SETTINGS_USB_ENABLED]) { + debug("Usb tethering settings is not enabled"); + return; + } + + if (this.tetheringSettings[SETTINGS_DUN_REQUIRED] && + aNetworkInfo.type === Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN && + this._tetheringInterface[TETHERING_TYPE_USB].externalInterface === + aNetworkInfo.name) { + debug("Dun required and dun interface is the same"); + return; + } + + if (this._tetheringInterface[TETHERING_TYPE_USB].externalInterface === + gNetworkManager.activeNetworkInfo.name) { + debug("The active interface is the same"); + return; + } + + let previous = { + internalIfname: this._tetheringInterface[TETHERING_TYPE_USB].internalInterface, + externalIfname: this._tetheringInterface[TETHERING_TYPE_USB].externalInterface + }; + + let current = { + internalIfname: this._tetheringInterface[TETHERING_TYPE_USB].internalInterface, + externalIfname: aNetworkInfo.name + }; + + let callback = (() => { + // Update external network interface. + debug("Update upstream interface to " + aNetworkInfo.name); + gNetworkService.updateUpStream(previous, current, + this.onConnectionChangedReport.bind(this)); + }); + + if (this._usbTetheringAction === TETHERING_STATE_ONGOING) { + debug("Postpone the event and handle it when state is idle."); + this.wantConnectionEvent = callback; + return; + } + this.wantConnectionEvent = null; + + callback.call(this); + }, + + isAnyConnected: function() { + let allNetworkInfo = gNetworkManager.allNetworkInfo; + for (let networkId in allNetworkInfo) { + if (allNetworkInfo.hasOwnProperty(networkId) && + allNetworkInfo[networkId].state === Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + return true; + } + } + return false; + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TetheringService]); diff --git a/dom/system/gonk/TetheringService.manifest b/dom/system/gonk/TetheringService.manifest new file mode 100644 index 000000000..b8f18dec1 --- /dev/null +++ b/dom/system/gonk/TetheringService.manifest @@ -0,0 +1,4 @@ +# TetheringService.js +component {527a4121-ee5a-4651-be9c-f46f59cf7c01} TetheringService.js +contract @mozilla.org/tethering/service;1 {527a4121-ee5a-4651-be9c-f46f59cf7c01} +category profile-after-change TetheringService @mozilla.org/tethering/service;1 diff --git a/dom/system/gonk/TimeZoneSettingObserver.cpp b/dom/system/gonk/TimeZoneSettingObserver.cpp new file mode 100644 index 000000000..512f79908 --- /dev/null +++ b/dom/system/gonk/TimeZoneSettingObserver.cpp @@ -0,0 +1,239 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "base/message_loop.h" +#include "jsapi.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/Attributes.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Hal.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsISettingsService.h" +#include "nsJSUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "TimeZoneSettingObserver.h" +#include "xpcpublic.h" +#include "nsContentUtils.h" +#include "nsPrintfCString.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" + +#undef LOG +#undef ERR +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "Time Zone Setting" , ## args) +#define ERR(args...) __android_log_print(ANDROID_LOG_ERROR, "Time Zone Setting" , ## args) + +#define TIME_TIMEZONE "time.timezone" +#define MOZSETTINGS_CHANGED "mozsettings-changed" + +using namespace mozilla; +using namespace mozilla::dom; + +namespace { + +class TimeZoneSettingObserver : public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + TimeZoneSettingObserver(); + static nsresult SetTimeZone(const JS::Value &aValue, JSContext *aContext); + +protected: + virtual ~TimeZoneSettingObserver(); +}; + +class TimeZoneSettingCb final : public nsISettingsServiceCallback +{ +public: + NS_DECL_ISUPPORTS + + TimeZoneSettingCb() {} + + NS_IMETHOD Handle(const nsAString &aName, JS::Handle<JS::Value> aResult) { + + JSContext *cx = nsContentUtils::GetCurrentJSContext(); + NS_ENSURE_TRUE(cx, NS_OK); + + // If we don't have time.timezone value in the settings, we need + // to initialize the settings based on the current system timezone + // to make settings consistent with system. This usually happens + // at the very first boot. After that, settings must have a value. + if (aResult.isNull()) { + // Get the current system time zone offset. Note that we need to + // convert the value to a UTC representation in the format of + // "UTC{+,-}hh:mm", so that the Gaia end can know how to interpret. + // E.g., -480 is "UTC+08:00"; 630 is "UTC-10:30". + int32_t timeZoneOffset = hal::GetTimezoneOffset(); + nsPrintfCString curTimeZone("UTC%+03d:%02d", + -timeZoneOffset / 60, + abs(timeZoneOffset) % 60); + + // Convert it to a JS string. + NS_ConvertUTF8toUTF16 utf16Str(curTimeZone); + + JS::Rooted<JSString*> jsStr(cx, JS_NewUCStringCopyN(cx, + utf16Str.get(), + utf16Str.Length())); + if (!jsStr) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Set the settings based on the current system timezone. + nsCOMPtr<nsISettingsServiceLock> lock; + nsCOMPtr<nsISettingsService> settingsService = + do_GetService("@mozilla.org/settingsService;1"); + if (!settingsService) { + ERR("Failed to get settingsLock service!"); + return NS_OK; + } + settingsService->CreateLock(nullptr, getter_AddRefs(lock)); + JS::Rooted<JS::Value> value(cx, JS::StringValue(jsStr)); + lock->Set(TIME_TIMEZONE, value, nullptr, nullptr); + return NS_OK; + } + + // Set the system timezone based on the current settings. + if (aResult.isString()) { + return TimeZoneSettingObserver::SetTimeZone(aResult, cx); + } + + return NS_OK; + } + + NS_IMETHOD HandleError(const nsAString &aName) { + ERR("TimeZoneSettingCb::HandleError: %s\n", NS_LossyConvertUTF16toASCII(aName).get()); + return NS_OK; + } + +protected: + ~TimeZoneSettingCb() {} +}; + +NS_IMPL_ISUPPORTS(TimeZoneSettingCb, nsISettingsServiceCallback) + +TimeZoneSettingObserver::TimeZoneSettingObserver() +{ + // Setup an observer to watch changes to the setting. + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + if (!observerService) { + ERR("GetObserverService failed"); + return; + } + nsresult rv; + rv = observerService->AddObserver(this, MOZSETTINGS_CHANGED, false); + if (NS_FAILED(rv)) { + ERR("AddObserver failed"); + return; + } + + // Read the 'time.timezone' setting in order to start with a known + // value at boot time. The handle() will be called after reading. + nsCOMPtr<nsISettingsServiceLock> lock; + nsCOMPtr<nsISettingsService> settingsService = + do_GetService("@mozilla.org/settingsService;1"); + if (!settingsService) { + ERR("Failed to get settingsLock service!"); + return; + } + settingsService->CreateLock(nullptr, getter_AddRefs(lock)); + nsCOMPtr<nsISettingsServiceCallback> callback = new TimeZoneSettingCb(); + lock->Get(TIME_TIMEZONE, callback); +} + +nsresult TimeZoneSettingObserver::SetTimeZone(const JS::Value &aValue, JSContext *aContext) +{ + // Convert the JS value to a nsCString type. + // The value should be a JS string like "America/Chicago" or "UTC-05:00". + nsAutoJSString valueStr; + if (!valueStr.init(aContext, aValue.toString())) { + ERR("Failed to convert JS value to nsCString"); + return NS_ERROR_FAILURE; + } + NS_ConvertUTF16toUTF8 newTimezone(valueStr); + + // Hal expects opposite sign from general notations, + // so we need to flip it. + if (newTimezone.Find(NS_LITERAL_CSTRING("UTC+")) == 0) { + if (!newTimezone.SetCharAt('-', 3)) { + return NS_ERROR_FAILURE; + } + } else if (newTimezone.Find(NS_LITERAL_CSTRING("UTC-")) == 0) { + if (!newTimezone.SetCharAt('+', 3)) { + return NS_ERROR_FAILURE; + } + } + + // Set the timezone only when the system timezone is not identical. + nsCString curTimezone = hal::GetTimezone(); + if (!curTimezone.Equals(newTimezone)) { + hal::SetTimezone(newTimezone); + } + + return NS_OK; +} + +TimeZoneSettingObserver::~TimeZoneSettingObserver() +{ + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, MOZSETTINGS_CHANGED); + } +} + +NS_IMPL_ISUPPORTS(TimeZoneSettingObserver, nsIObserver) + +NS_IMETHODIMP +TimeZoneSettingObserver::Observe(nsISupports *aSubject, + const char *aTopic, + const char16_t *aData) +{ + if (strcmp(aTopic, MOZSETTINGS_CHANGED) != 0) { + return NS_OK; + } + + // Note that this function gets called for any and all settings changes, + // so we need to carefully check if we have the one we're interested in. + // + // The string that we're interested in will be a JSON string that looks like: + // {"key":"time.timezone","value":"America/Chicago"} or + // {"key":"time.timezone","value":"UTC-05:00"} + + AutoSafeJSContext cx; + RootedDictionary<SettingChangeNotification> setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { + return NS_OK; + } + if (!setting.mKey.EqualsASCII(TIME_TIMEZONE)) { + return NS_OK; + } + if (!setting.mValue.isString()) { + return NS_OK; + } + + // Set the system timezone. + return SetTimeZone(setting.mValue, cx); +} + +} // namespace + +static mozilla::StaticRefPtr<TimeZoneSettingObserver> sTimeZoneSettingObserver; +namespace mozilla { +namespace system { +void +InitializeTimeZoneSettingObserver() +{ + sTimeZoneSettingObserver = new TimeZoneSettingObserver(); + ClearOnShutdown(&sTimeZoneSettingObserver); +} + +} // namespace system +} // namespace mozilla diff --git a/dom/system/gonk/TimeZoneSettingObserver.h b/dom/system/gonk/TimeZoneSettingObserver.h new file mode 100644 index 000000000..08b7cebb3 --- /dev/null +++ b/dom/system/gonk/TimeZoneSettingObserver.h @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_timesetting_h__ +#define mozilla_system_timesetting_h__ + +namespace mozilla { +namespace system { + +// Initialize TimeZoneSettingObserver which observes the time zone change +// event from settings service. When receiving the event, it modifies the +// system time zone. +void InitializeTimeZoneSettingObserver(); + +} // namespace system +} // namespace mozilla + +#endif // mozilla_system_timesetting_h__ + diff --git a/dom/system/gonk/Volume.cpp b/dom/system/gonk/Volume.cpp new file mode 100644 index 000000000..f90c7b693 --- /dev/null +++ b/dom/system/gonk/Volume.cpp @@ -0,0 +1,596 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Volume.h" +#include "VolumeCommand.h" +#include "VolumeManager.h" +#include "VolumeManagerLog.h" +#include "nsIVolume.h" +#include "nsXULAppAPI.h" + +#include <vold/ResponseCode.h> + +namespace mozilla { +namespace system { + +#if DEBUG_VOLUME_OBSERVER +void +VolumeObserverList::Broadcast(Volume* const& aVolume) +{ + uint32_t size = mObservers.Length(); + for (uint32_t i = 0; i < size; ++i) { + LOG("VolumeObserverList::Broadcast to [%u] %p volume '%s'", + i, mObservers[i], aVolume->NameStr()); + mObservers[i]->Notify(aVolume); + } +} +#endif + +VolumeObserverList Volume::sEventObserverList; + +// We have a feature where volumes can be locked when mounted. This +// is used to prevent a volume from being shared with the PC while +// it is actively being used (say for storing an update image) +// +// We use WakeLocks (a poor choice of name, but it does what we want) +// from the PowerManagerService to determine when we're locked. +// In particular we'll create a wakelock called volume-NAME-GENERATION +// (where NAME is the volume name, and GENERATION is its generation +// number), and if this wakelock is locked, then we'll prevent a volume +// from being shared. +// +// Implementation Details: +// +// Since the AutoMounter can only control when something gets mounted +// and not when it gets unmounted (for example: a user pulls the SDCard) +// and because Volume and nsVolume data structures are maintained on +// separate threads, we have the potential for some race conditions. +// We eliminate the race conditions by introducing the concept of a +// generation number. Every time a volume transitions to the Mounted +// state, it gets assigned a new generation number. Whenever the state +// of a Volume changes, we send the updated state and current generation +// number to the main thread where it gets updated in the nsVolume. +// +// Since WakeLocks can only be queried from the main-thread, the +// nsVolumeService looks for WakeLock status changes, and forwards +// the results to the IOThread. +// +// If the Volume (IOThread) receives a volume update where the generation +// number mismatches, then the update is simply ignored. +// +// When a Volume (IOThread) initially becomes mounted, we assume it to +// be locked until we get our first update from nsVolume (MainThread). +static int32_t sMountGeneration = 0; + +static uint32_t sNextId = 1; + +// We don't get media inserted/removed events at startup. So we +// assume it's present, and we'll be told that it's missing. +Volume::Volume(const nsCSubstring& aName) + : mMediaPresent(true), + mState(nsIVolume::STATE_INIT), + mName(aName), + mMountGeneration(-1), + mMountLocked(true), // Needs to agree with nsVolume::nsVolume + mSharingEnabled(false), + mFormatRequested(false), + mMountRequested(false), + mUnmountRequested(false), + mCanBeShared(true), + mIsSharing(false), + mIsFormatting(false), + mIsUnmounting(false), + mIsRemovable(false), + mIsHotSwappable(false), + mId(sNextId++) +{ + DBG("Volume %s: created", NameStr()); +} + +void +Volume::Dump(const char* aLabel) const +{ + LOG("%s: Volume: %s (%d) is %s and %s @ %s gen %d locked %d", + aLabel, + NameStr(), + Id(), + StateStr(), + MediaPresent() ? "inserted" : "missing", + MountPoint().get(), + MountGeneration(), + (int)IsMountLocked()); + LOG("%s: Sharing %s Mounting %s Formating %s Unmounting %s", + aLabel, + CanBeShared() ? (IsSharingEnabled() ? (IsSharing() ? "en-y" : "en-n") + : "dis") + : "x", + IsMountRequested() ? "req" : "n", + IsFormatRequested() ? (IsFormatting() ? "req-y" : "req-n") + : (IsFormatting() ? "y" : "n"), + IsUnmountRequested() ? (IsUnmounting() ? "req-y" : "req-n") + : (IsUnmounting() ? "y" : "n")); +} + +void +Volume::ResolveAndSetMountPoint(const nsCSubstring& aMountPoint) +{ + nsCString mountPoint(aMountPoint); + char realPathBuf[PATH_MAX]; + + // Call realpath so that we wind up with a path which is compatible with + // functions like nsVolumeService::GetVolumeByPath. + + if (realpath(mountPoint.get(), realPathBuf) < 0) { + // The path we were handed doesn't exist. Warn about it, but use it + // anyways assuming that the user knows what they're doing. + + ERR("ResolveAndSetMountPoint: realpath on '%s' failed: %d", + mountPoint.get(), errno); + mMountPoint = mountPoint; + } else { + mMountPoint = realPathBuf; + } + DBG("Volume %s: Setting mountpoint to '%s'", NameStr(), mMountPoint.get()); +} + +void Volume::SetFakeVolume(const nsACString& aMountPoint) +{ + this->mMountLocked = false; + this->mCanBeShared = false; + ResolveAndSetMountPoint(aMountPoint); + SetState(nsIVolume::STATE_MOUNTED); +} + +void +Volume::SetIsSharing(bool aIsSharing) +{ + if (aIsSharing == mIsSharing) { + return; + } + mIsSharing = aIsSharing; + LOG("Volume %s: IsSharing set to %d state %s", + NameStr(), (int)mIsSharing, StateStr(mState)); + sEventObserverList.Broadcast(this); +} + +void +Volume::SetIsFormatting(bool aIsFormatting) +{ + if (aIsFormatting == mIsFormatting) { + return; + } + mIsFormatting = aIsFormatting; + LOG("Volume %s: IsFormatting set to %d state %s", + NameStr(), (int)mIsFormatting, StateStr(mState)); + if (mIsFormatting) { + sEventObserverList.Broadcast(this); + } +} + +void +Volume::SetIsUnmounting(bool aIsUnmounting) +{ + if (aIsUnmounting == mIsUnmounting) { + return; + } + mIsUnmounting = aIsUnmounting; + LOG("Volume %s: IsUnmounting set to %d state %s", + NameStr(), (int)mIsUnmounting, StateStr(mState)); + sEventObserverList.Broadcast(this); +} + +void +Volume::SetIsRemovable(bool aIsRemovable) +{ + if (aIsRemovable == mIsRemovable) { + return; + } + mIsRemovable = aIsRemovable; + if (!mIsRemovable) { + mIsHotSwappable = false; + } + LOG("Volume %s: IsRemovable set to %d state %s", + NameStr(), (int)mIsRemovable, StateStr(mState)); + sEventObserverList.Broadcast(this); +} + +void +Volume::SetIsHotSwappable(bool aIsHotSwappable) +{ + if (aIsHotSwappable == mIsHotSwappable) { + return; + } + mIsHotSwappable = aIsHotSwappable; + if (mIsHotSwappable) { + mIsRemovable = true; + } + LOG("Volume %s: IsHotSwappable set to %d state %s", + NameStr(), (int)mIsHotSwappable, StateStr(mState)); + sEventObserverList.Broadcast(this); +} + +bool +Volume::BoolConfigValue(const nsCString& aConfigValue, bool& aBoolValue) +{ + if (aConfigValue.EqualsLiteral("1") || + aConfigValue.LowerCaseEqualsLiteral("true")) { + aBoolValue = true; + return true; + } + if (aConfigValue.EqualsLiteral("0") || + aConfigValue.LowerCaseEqualsLiteral("false")) { + aBoolValue = false; + return true; + } + return false; +} + +void +Volume::SetConfig(const nsCString& aConfigName, const nsCString& aConfigValue) +{ + if (aConfigName.LowerCaseEqualsLiteral("removable")) { + bool value = false; + if (BoolConfigValue(aConfigValue, value)) { + SetIsRemovable(value); + } else { + ERR("Volume %s: invalid value '%s' for configuration '%s'", + NameStr(), aConfigValue.get(), aConfigName.get()); + } + return; + } + if (aConfigName.LowerCaseEqualsLiteral("hotswappable")) { + bool value = false; + if (BoolConfigValue(aConfigValue, value)) { + SetIsHotSwappable(value); + } else { + ERR("Volume %s: invalid value '%s' for configuration '%s'", + NameStr(), aConfigValue.get(), aConfigName.get()); + } + return; + } + ERR("Volume %s: invalid config '%s'", NameStr(), aConfigName.get()); +} + +void +Volume::SetMediaPresent(bool aMediaPresent) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + // mMediaPresent is slightly redunant to the state, however + // when media is removed (while Idle), we get the following: + // 631 Volume sdcard /mnt/sdcard disk removed (179:0) + // 605 Volume sdcard /mnt/sdcard state changed from 1 (Idle-Unmounted) to 0 (No-Media) + // + // And on media insertion, we get: + // 630 Volume sdcard /mnt/sdcard disk inserted (179:0) + // 605 Volume sdcard /mnt/sdcard state changed from 0 (No-Media) to 2 (Pending) + // 605 Volume sdcard /mnt/sdcard state changed from 2 (Pending) to 1 (Idle-Unmounted) + // + // On media removal while the media is mounted: + // 632 Volume sdcard /mnt/sdcard bad removal (179:1) + // 605 Volume sdcard /mnt/sdcard state changed from 4 (Mounted) to 5 (Unmounting) + // 605 Volume sdcard /mnt/sdcard state changed from 5 (Unmounting) to 1 (Idle-Unmounted) + // 631 Volume sdcard /mnt/sdcard disk removed (179:0) + // 605 Volume sdcard /mnt/sdcard state changed from 1 (Idle-Unmounted) to 0 (No-Media) + // + // When sharing with a PC, it goes Mounted -> Idle -> Shared + // When unsharing with a PC, it goes Shared -> Idle -> Mounted + // + // The AutoMounter needs to know whether the media is present or not when + // processing the Idle state. + + if (mMediaPresent == aMediaPresent) { + return; + } + + LOG("Volume: %s media %s", NameStr(), aMediaPresent ? "inserted" : "removed"); + mMediaPresent = aMediaPresent; + sEventObserverList.Broadcast(this); +} + +void +Volume::SetSharingEnabled(bool aSharingEnabled) +{ + mSharingEnabled = aSharingEnabled; + + LOG("SetSharingMode for volume %s to %d canBeShared = %d", + NameStr(), (int)mSharingEnabled, (int)mCanBeShared); + sEventObserverList.Broadcast(this); +} + +void +Volume::SetFormatRequested(bool aFormatRequested) +{ + mFormatRequested = aFormatRequested; + + LOG("SetFormatRequested for volume %s to %d CanBeFormatted = %d", + NameStr(), (int)mFormatRequested, (int)CanBeFormatted()); +} + +void +Volume::SetMountRequested(bool aMountRequested) +{ + mMountRequested = aMountRequested; + + LOG("SetMountRequested for volume %s to %d CanBeMounted = %d", + NameStr(), (int)mMountRequested, (int)CanBeMounted()); +} + +void +Volume::SetUnmountRequested(bool aUnmountRequested) +{ + mUnmountRequested = aUnmountRequested; + + LOG("SetUnmountRequested for volume %s to %d CanBeMounted = %d", + NameStr(), (int)mUnmountRequested, (int)CanBeMounted()); +} + +void +Volume::SetState(Volume::STATE aNewState) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + if (aNewState == mState) { + return; + } + if (aNewState == nsIVolume::STATE_MOUNTED) { + mMountGeneration = ++sMountGeneration; + LOG("Volume %s (%u): changing state from %s to %s @ '%s' (%d observers) " + "mountGeneration = %d, locked = %d", + NameStr(), mId, StateStr(mState), + StateStr(aNewState), mMountPoint.get(), sEventObserverList.Length(), + mMountGeneration, (int)mMountLocked); + } else { + LOG("Volume %s (%u): changing state from %s to %s (%d observers)", + NameStr(), mId, StateStr(mState), + StateStr(aNewState), sEventObserverList.Length()); + } + + switch (aNewState) { + case nsIVolume::STATE_NOMEDIA: + // Cover the startup case where we don't get insertion/removal events + mMediaPresent = false; + mIsSharing = false; + mUnmountRequested = false; + mMountRequested = false; + mIsUnmounting = false; + break; + + case nsIVolume::STATE_MOUNTED: + case nsIVolume::STATE_MOUNT_FAIL: + mMountRequested = false; + mIsFormatting = false; + mIsSharing = false; + mIsUnmounting = false; + break; + + case nsIVolume::STATE_FORMATTING: + mFormatRequested = false; + mIsFormatting = true; + mIsSharing = false; + mIsUnmounting = false; + break; + + case nsIVolume::STATE_SHARED: + case nsIVolume::STATE_SHAREDMNT: + // Covers startup cases. Normally, mIsSharing would be set to true + // when we issue the command to initiate the sharing process, but + // it's conceivable that a volume could already be in a shared state + // when b2g starts. + mIsSharing = true; + mIsUnmounting = false; + mIsFormatting = false; + break; + + case nsIVolume::STATE_UNMOUNTING: + mIsUnmounting = true; + mIsFormatting = false; + mIsSharing = false; + break; + + case nsIVolume::STATE_IDLE: // Fall through + case nsIVolume::STATE_CHECKMNT: // Fall through + default: + break; + } + mState = aNewState; + sEventObserverList.Broadcast(this); +} + +void +Volume::SetMountPoint(const nsCSubstring& aMountPoint) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + if (mMountPoint.Equals(aMountPoint)) { + return; + } + ResolveAndSetMountPoint(aMountPoint); +} + +void +Volume::StartMount(VolumeResponseCallback* aCallback) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + StartCommand(new VolumeActionCommand(this, "mount", "", aCallback)); +} + +void +Volume::StartUnmount(VolumeResponseCallback* aCallback) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + StartCommand(new VolumeActionCommand(this, "unmount", "force", aCallback)); +} + +void +Volume::StartFormat(VolumeResponseCallback* aCallback) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + StartCommand(new VolumeActionCommand(this, "format", "", aCallback)); +} + +void +Volume::StartShare(VolumeResponseCallback* aCallback) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + StartCommand(new VolumeActionCommand(this, "share", "ums", aCallback)); +} + +void +Volume::StartUnshare(VolumeResponseCallback* aCallback) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + StartCommand(new VolumeActionCommand(this, "unshare", "ums", aCallback)); +} + +void +Volume::StartCommand(VolumeCommand* aCommand) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + VolumeManager::PostCommand(aCommand); +} + +//static +void +Volume::RegisterVolumeObserver(Volume::EventObserver* aObserver, const char* aName) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + sEventObserverList.AddObserver(aObserver); + + DBG("Added Volume Observer '%s' @%p, length = %u", + aName, aObserver, sEventObserverList.Length()); + + // Send an initial event to the observer (for each volume) + size_t numVolumes = VolumeManager::NumVolumes(); + for (size_t volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex); + aObserver->Notify(vol); + } +} + +//static +void +Volume::UnregisterVolumeObserver(Volume::EventObserver* aObserver, const char* aName) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + sEventObserverList.RemoveObserver(aObserver); + + DBG("Removed Volume Observer '%s' @%p, length = %u", + aName, aObserver, sEventObserverList.Length()); +} + +//static +void +Volume::UpdateMountLock(const nsACString& aVolumeName, + const int32_t& aMountGeneration, + const bool& aMountLocked) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName); + if (!vol || (vol->mMountGeneration != aMountGeneration)) { + return; + } + if (vol->mMountLocked != aMountLocked) { + vol->mMountLocked = aMountLocked; + DBG("Volume::UpdateMountLock for '%s' to %d\n", vol->NameStr(), (int)aMountLocked); + sEventObserverList.Broadcast(vol); + } +} + +void +Volume::HandleVoldResponse(int aResponseCode, nsCWhitespaceTokenizer& aTokenizer) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + // The volume name will have already been parsed, and the tokenizer will point + // to the token after the volume name + switch (aResponseCode) { + case ::ResponseCode::VolumeListResult: { + // Each line will look something like: + // + // sdcard /mnt/sdcard 1 + // + nsDependentCSubstring mntPoint(aTokenizer.nextToken()); + SetMountPoint(mntPoint); + nsresult errCode; + nsCString state(aTokenizer.nextToken()); + if (state.EqualsLiteral("X")) { + // Special state for creating fake volumes which can't be shared. + mCanBeShared = false; + SetState(nsIVolume::STATE_MOUNTED); + } else { + SetState((STATE)state.ToInteger(&errCode)); + } + break; + } + + case ::ResponseCode::VolumeStateChange: { + // Format of the line looks something like: + // + // Volume sdcard /mnt/sdcard state changed from 7 (Shared-Unmounted) to 1 (Idle-Unmounted) + // + // So we parse out the state after the string " to " + while (aTokenizer.hasMoreTokens()) { + nsAutoCString token(aTokenizer.nextToken()); + if (token.EqualsLiteral("to")) { + nsresult errCode; + token = aTokenizer.nextToken(); + STATE newState = (STATE)(token.ToInteger(&errCode)); + if (newState == nsIVolume::STATE_MOUNTED) { + // We set the state to STATE_CHECKMNT here, and the once the + // AutoMounter detects that the volume is actually accessible + // then the AutoMounter will set the volume as STATE_MOUNTED. + SetState(nsIVolume::STATE_CHECKMNT); + } else { + if (State() == nsIVolume::STATE_CHECKING && newState == nsIVolume::STATE_IDLE) { + LOG("Mount of volume '%s' failed", NameStr()); + SetState(nsIVolume::STATE_MOUNT_FAIL); + } else { + SetState(newState); + } + } + break; + } + } + break; + } + + case ::ResponseCode::VolumeDiskInserted: + SetMediaPresent(true); + break; + + case ::ResponseCode::VolumeDiskRemoved: // fall-thru + case ::ResponseCode::VolumeBadRemoval: + SetMediaPresent(false); + break; + + default: + LOG("Volume: %s unrecognized reponse code (ignored)", NameStr()); + break; + } +} + +} // namespace system +} // namespace mozilla diff --git a/dom/system/gonk/Volume.h b/dom/system/gonk/Volume.h new file mode 100644 index 000000000..821292a9a --- /dev/null +++ b/dom/system/gonk/Volume.h @@ -0,0 +1,157 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_volume_h__ +#define mozilla_system_volume_h__ + +#include "VolumeCommand.h" +#include "nsIVolume.h" +#include "nsString.h" +#include "mozilla/Observer.h" +#include "nsISupportsImpl.h" +#include "nsWhitespaceTokenizer.h" + +namespace mozilla { +namespace system { + +/*************************************************************************** +* +* There is an instance of the Volume class for each volume reported +* from vold. +* +* Each volume originates from the /system/etv/vold.fstab file. +* +***************************************************************************/ + +class Volume; + +#define DEBUG_VOLUME_OBSERVER 0 + +#if DEBUG_VOLUME_OBSERVER +class VolumeObserverList : public mozilla::ObserverList<Volume*> +{ +public: + void Broadcast(Volume* const& aVolume); +}; +#else +typedef mozilla::ObserverList<Volume*> VolumeObserverList; +#endif + +class Volume final +{ +public: + NS_INLINE_DECL_REFCOUNTING(Volume) + + Volume(const nsCSubstring& aVolumeName); + + typedef long STATE; // States are now defined in nsIVolume.idl + + static const char* StateStr(STATE aState) { return NS_VolumeStateStr(aState); } + const char* StateStr() const { return StateStr(mState); } + STATE State() const { return mState; } + + const nsCString& Name() const { return mName; } + const char* NameStr() const { return mName.get(); } + + void Dump(const char* aLabel) const; + + // The mount point is the name of the directory where the volume is mounted. + // (i.e. path that leads to the files stored on the volume). + const nsCString& MountPoint() const { return mMountPoint; } + + uint32_t Id() const { return mId; } + + int32_t MountGeneration() const { return mMountGeneration; } + bool IsMountLocked() const { return mMountLocked; } + bool MediaPresent() const { return mMediaPresent; } + bool CanBeShared() const { return mCanBeShared; } + bool CanBeFormatted() const { return CanBeShared(); } + bool CanBeMounted() const { return CanBeShared(); } + bool IsSharingEnabled() const { return mCanBeShared && mSharingEnabled; } + bool IsFormatRequested() const { return CanBeFormatted() && mFormatRequested; } + bool IsMountRequested() const { return CanBeMounted() && mMountRequested; } + bool IsUnmountRequested() const { return CanBeMounted() && mUnmountRequested; } + bool IsSharing() const { return mIsSharing; } + bool IsFormatting() const { return mIsFormatting; } + bool IsUnmounting() const { return mIsUnmounting; } + bool IsRemovable() const { return mIsRemovable; } + bool IsHotSwappable() const { return mIsHotSwappable; } + + void SetFakeVolume(const nsACString& aMountPoint); + + void SetSharingEnabled(bool aSharingEnabled); + void SetFormatRequested(bool aFormatRequested); + void SetMountRequested(bool aMountRequested); + void SetUnmountRequested(bool aUnmountRequested); + + typedef mozilla::Observer<Volume *> EventObserver; + + // NOTE: that observers must live in the IOThread. + static void RegisterVolumeObserver(EventObserver* aObserver, const char* aName); + static void UnregisterVolumeObserver(EventObserver* aObserver, const char* aName); + +protected: + ~Volume() {} + +private: + friend class AutoMounter; // Calls StartXxx + friend class nsVolume; // Calls UpdateMountLock + friend class VolumeManager; // Calls HandleVoldResponse + friend class VolumeListCallback; // Calls SetMountPoint, SetState + + // The StartXxx functions will queue up a command to the VolumeManager. + // You can queue up as many commands as you like, and aCallback will + // be called as each one completes. + void StartMount(VolumeResponseCallback* aCallback); + void StartUnmount(VolumeResponseCallback* aCallback); + void StartFormat(VolumeResponseCallback* aCallback); + void StartShare(VolumeResponseCallback* aCallback); + void StartUnshare(VolumeResponseCallback* aCallback); + + void SetIsSharing(bool aIsSharing); + void SetIsFormatting(bool aIsFormatting); + void SetIsUnmounting(bool aIsUnmounting); + void SetIsRemovable(bool aIsRemovable); + void SetIsHotSwappable(bool aIsHotSwappable); + void SetState(STATE aNewState); + void SetMediaPresent(bool aMediaPresent); + void SetMountPoint(const nsCSubstring& aMountPoint); + void StartCommand(VolumeCommand* aCommand); + + void ResolveAndSetMountPoint(const nsCSubstring& aMountPoint); + + bool BoolConfigValue(const nsCString& aConfigValue, bool& aBoolValue); + void SetConfig(const nsCString& aConfigName, const nsCString& aConfigValue); + + void HandleVoldResponse(int aResponseCode, nsCWhitespaceTokenizer& aTokenizer); + + static void UpdateMountLock(const nsACString& aVolumeName, + const int32_t& aMountGeneration, + const bool& aMountLocked); + + bool mMediaPresent; + STATE mState; + const nsCString mName; + nsCString mMountPoint; + int32_t mMountGeneration; + bool mMountLocked; + bool mSharingEnabled; + bool mFormatRequested; + bool mMountRequested; + bool mUnmountRequested; + bool mCanBeShared; + bool mIsSharing; + bool mIsFormatting; + bool mIsUnmounting; + bool mIsRemovable; + bool mIsHotSwappable; + uint32_t mId; // Unique ID (used by MTP) + + static VolumeObserverList sEventObserverList; +}; + +} // system +} // mozilla + +#endif // mozilla_system_volumemanager_h__ diff --git a/dom/system/gonk/VolumeCommand.cpp b/dom/system/gonk/VolumeCommand.cpp new file mode 100644 index 000000000..8095956a7 --- /dev/null +++ b/dom/system/gonk/VolumeCommand.cpp @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsString.h" +#include "nsWhitespaceTokenizer.h" + +#include "Volume.h" +#include "VolumeCommand.h" +#include "VolumeManager.h" +#include "VolumeManagerLog.h" + +namespace mozilla { +namespace system { + +/*************************************************************************** +* +* The VolumeActionCommand class is used to send commands which apply +* to a particular volume. +* +* The following commands would fit into this category: +* +* volume mount <volname> +* volume unmount <volname> [force] +* volume format <volname> +* volume share <volname> <method> +* volume unshare <volname> <method> +* volume shared <volname> <method> +* +* A typical response looks like: +* +* # vdc volume unshare sdcard ums +* 605 Volume sdcard /mnt/sdcard state changed from 7 (Shared-Unmounted) to 1 (Idle-Unmounted) +* 200 volume operation succeeded +* +* Note that the 600 series of responses are considered unsolicited and +* are dealt with directly by the VolumeManager. This command will only +* see the terminating response code (200 in the example above). +* +***************************************************************************/ + +VolumeActionCommand::VolumeActionCommand(Volume* aVolume, + const char* aAction, + const char* aExtraArgs, + VolumeResponseCallback* aCallback) + : VolumeCommand(aCallback), + mVolume(aVolume) +{ + nsAutoCString cmd; + + cmd = "volume "; + cmd += aAction; + cmd += " "; + cmd += aVolume->Name().get(); + + // vold doesn't like trailing white space, so only add it if we really need to. + if (aExtraArgs && (*aExtraArgs != '\0')) { + cmd += " "; + cmd += aExtraArgs; + } + SetCmd(cmd); +} + +/*************************************************************************** +* +* The VolumeListCommand class is used to send the "volume list" command to +* vold. +* +* A typical response looks like: +* +* # vdc volume list +* 110 sdcard /mnt/sdcard 4 +* 110 sdcard1 /mnt/sdcard/external_sd 4 +* 200 Volumes listed. +* +***************************************************************************/ + +VolumeListCommand::VolumeListCommand(VolumeResponseCallback* aCallback) + : VolumeCommand(NS_LITERAL_CSTRING("volume list"), aCallback) +{ +} + +} // system +} // mozilla + diff --git a/dom/system/gonk/VolumeCommand.h b/dom/system/gonk/VolumeCommand.h new file mode 100644 index 000000000..022965b5e --- /dev/null +++ b/dom/system/gonk/VolumeCommand.h @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_volumecommand_h__ +#define mozilla_system_volumecommand_h__ + +#include "nsString.h" +#include "nsISupportsImpl.h" +#include "mozilla/RefPtr.h" +#include <algorithm> +#include <vold/ResponseCode.h> + +namespace mozilla { +namespace system { + +class Volume; +class VolumeCommand; + +/*************************************************************************** +* +* The VolumeResponseCallback class is an abstract base class. The ResponseReceived +* method will be called for each response received. +* +* Depending on the command, there may be multiple responses for the +* command. Done() will return true if this is the last response. +* +* The responses from vold are all of the form: +* +* <ResponseCode> <String> +* +* Valid Response codes can be found in the vold/ResponseCode.h header. +* +***************************************************************************/ + +class VolumeResponseCallback +{ +protected: + virtual ~VolumeResponseCallback() {} + +public: + NS_INLINE_DECL_REFCOUNTING(VolumeResponseCallback) + VolumeResponseCallback() + : mResponseCode(0), mPending(false) {} + + bool Done() const + { + // Response codes from the 200, 400, and 500 series all indicated that + // the command has completed. + + return (mResponseCode >= ::ResponseCode::CommandOkay) + && (mResponseCode < ::ResponseCode::UnsolicitedInformational); + } + + bool WasSuccessful() const + { + return mResponseCode == ::ResponseCode::CommandOkay; + } + + bool IsPending() const { return mPending; } + int ResponseCode() const { return mResponseCode; } + const nsCString &ResponseStr() const { return mResponseStr; } + +protected: + virtual void ResponseReceived(const VolumeCommand* aCommand) = 0; + +private: + friend class VolumeCommand; // Calls HandleResponse and SetPending + + void HandleResponse(const VolumeCommand* aCommand, + int aResponseCode, + nsACString& aResponseStr) + { + mResponseCode = aResponseCode; +#if ANDROID_VERSION >= 17 + // There's a sequence number here that we don't care about + // We expect it to be 0. See VolumeCommand::SetCmd + mResponseStr = Substring(aResponseStr, 2); +#else + mResponseStr = aResponseStr; +#endif + if (mResponseCode >= ::ResponseCode::CommandOkay) { + // This is a final response. + mPending = false; + } + ResponseReceived(aCommand); + } + + void SetPending(bool aPending) { mPending = aPending; } + + int mResponseCode; // The response code parsed from vold + nsCString mResponseStr; // The rest of the line. + bool mPending; // Waiting for response? +}; + +/*************************************************************************** +* +* The VolumeCommand class is an abstract base class used to encapsulate +* volume commands send to vold. +* +* See VolumeManager.h for a list of the volume commands. +* +* Commands sent to vold need an explicit null character so we add one +* to the command to ensure that it's included in the length. +* +* All of these commands are asynchronous in nature, and the +* ResponseReceived callback will be called when a response is available. +* +***************************************************************************/ + +class VolumeCommand +{ +protected: + virtual ~VolumeCommand() {} + +public: + NS_INLINE_DECL_REFCOUNTING(VolumeCommand) + + VolumeCommand(VolumeResponseCallback* aCallback) + : mBytesConsumed(0), + mCallback(aCallback) + { + SetCmd(NS_LITERAL_CSTRING("")); + } + + VolumeCommand(const nsACString& aCommand, VolumeResponseCallback* aCallback) + : mBytesConsumed(0), + mCallback(aCallback) + { + SetCmd(aCommand); + } + + void SetCmd(const nsACString& aCommand) + { + mCmd.Truncate(); +#if ANDROID_VERSION >= 17 + // JB requires a sequence number at the beginning of messages. + // It doesn't matter what we use, so we use 0. + mCmd = "0 "; +#endif + mCmd.Append(aCommand); + // Add a null character. We want this to be included in the length since + // vold uses it to determine the end of the command. + mCmd.Append('\0'); + } + + const char* CmdStr() const { return mCmd.get(); } + const char* Data() const { return mCmd.Data() + mBytesConsumed; } + size_t BytesConsumed() const { return mBytesConsumed; } + + size_t BytesRemaining() const + { + return mCmd.Length() - std::min(mCmd.Length(), mBytesConsumed); + } + + void ConsumeBytes(size_t aNumBytes) + { + mBytesConsumed += std::min(BytesRemaining(), aNumBytes); + } + +private: + friend class VolumeManager; // Calls SetPending & HandleResponse + + void SetPending(bool aPending) + { + if (mCallback) { + mCallback->SetPending(aPending); + } + } + + void HandleResponse(int aResponseCode, nsACString& aResponseStr) + { + if (mCallback) { + mCallback->HandleResponse(this, aResponseCode, aResponseStr); + } + } + + nsCString mCmd; // Command being sent + size_t mBytesConsumed; // How many bytes have been sent + + // Called when a response to the command is received. + RefPtr<VolumeResponseCallback> mCallback; +}; + +class VolumeActionCommand : public VolumeCommand +{ +public: + VolumeActionCommand(Volume* aVolume, const char* aAction, + const char* aExtraArgs, VolumeResponseCallback* aCallback); + +private: + RefPtr<Volume> mVolume; +}; + +class VolumeListCommand : public VolumeCommand +{ +public: + VolumeListCommand(VolumeResponseCallback* aCallback); +}; + +} // system +} // mozilla + +#endif // mozilla_system_volumecommand_h__ diff --git a/dom/system/gonk/VolumeManager.cpp b/dom/system/gonk/VolumeManager.cpp new file mode 100644 index 000000000..ddfa7af09 --- /dev/null +++ b/dom/system/gonk/VolumeManager.cpp @@ -0,0 +1,591 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "VolumeManager.h" + +#include "Volume.h" +#include "VolumeCommand.h" +#include "VolumeManagerLog.h" +#include "VolumeServiceTest.h" + +#include "nsWhitespaceTokenizer.h" +#include "nsXULAppAPI.h" + +#include "base/message_loop.h" +#include "base/task.h" +#include "mozilla/Scoped.h" +#include "mozilla/StaticPtr.h" + +#include <android/log.h> +#include <cutils/sockets.h> +#include <fcntl.h> +#include <sys/socket.h> + +namespace mozilla { +namespace system { + +static StaticRefPtr<VolumeManager> sVolumeManager; + +VolumeManager::STATE VolumeManager::mState = VolumeManager::UNINITIALIZED; +VolumeManager::StateObserverList VolumeManager::mStateObserverList; + +/***************************************************************************/ + +VolumeManager::VolumeManager() + : LineWatcher('\0', kRcvBufSize), + mSocket(-1), + mCommandPending(false) +{ + DBG("VolumeManager constructor called"); +} + +VolumeManager::~VolumeManager() +{ +} + +//static +void +VolumeManager::Dump(const char* aLabel) +{ + if (!sVolumeManager) { + LOG("%s: sVolumeManager == null", aLabel); + return; + } + + VolumeArray::size_type numVolumes = NumVolumes(); + VolumeArray::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<Volume> vol = GetVolume(volIndex); + vol->Dump(aLabel); + } +} + +//static +size_t +VolumeManager::NumVolumes() +{ + if (!sVolumeManager) { + return 0; + } + return sVolumeManager->mVolumeArray.Length(); +} + +//static +already_AddRefed<Volume> +VolumeManager::GetVolume(size_t aIndex) +{ + MOZ_ASSERT(aIndex < NumVolumes()); + RefPtr<Volume> vol = sVolumeManager->mVolumeArray[aIndex]; + return vol.forget(); +} + +//static +VolumeManager::STATE +VolumeManager::State() +{ + return mState; +} + +//static +const char * +VolumeManager::StateStr(VolumeManager::STATE aState) +{ + switch (aState) { + case UNINITIALIZED: return "Uninitialized"; + case STARTING: return "Starting"; + case VOLUMES_READY: return "Volumes Ready"; + } + return "???"; +} + + +//static +void +VolumeManager::SetState(STATE aNewState) +{ + if (mState != aNewState) { + LOG("changing state from '%s' to '%s'", + StateStr(mState), StateStr(aNewState)); + mState = aNewState; + mStateObserverList.Broadcast(StateChangedEvent()); + } +} + +//static +void +VolumeManager::RegisterStateObserver(StateObserver* aObserver) +{ + mStateObserverList.AddObserver(aObserver); +} + +//static +void VolumeManager::UnregisterStateObserver(StateObserver* aObserver) +{ + mStateObserverList.RemoveObserver(aObserver); +} + +//static +already_AddRefed<Volume> +VolumeManager::FindVolumeByName(const nsCSubstring& aName) +{ + if (!sVolumeManager) { + return nullptr; + } + VolumeArray::size_type numVolumes = NumVolumes(); + VolumeArray::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<Volume> vol = GetVolume(volIndex); + if (vol->Name().Equals(aName)) { + return vol.forget(); + } + } + return nullptr; +} + +//static +already_AddRefed<Volume> +VolumeManager::FindAddVolumeByName(const nsCSubstring& aName) +{ + RefPtr<Volume> vol = FindVolumeByName(aName); + if (vol) { + return vol.forget(); + } + // No volume found, create and add a new one. + vol = new Volume(aName); + sVolumeManager->mVolumeArray.AppendElement(vol); + return vol.forget(); +} + +//static +bool +VolumeManager::RemoveVolumeByName(const nsCSubstring& aName) +{ + if (!sVolumeManager) { + return false; + } + VolumeArray::size_type numVolumes = NumVolumes(); + VolumeArray::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<Volume> vol = GetVolume(volIndex); + if (vol->Name().Equals(aName)) { + sVolumeManager->mVolumeArray.RemoveElementAt(volIndex); + return true; + } + } + // No volume found. Return false to indicate this. + return false; +} + + +//static +void VolumeManager::InitConfig() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + // This function uses /system/etc/volume.cfg to add additional volumes + // to the Volume Manager. + // + // This is useful on devices like the Nexus 4, which have no physical sd card + // or dedicated partition. + // + // The format of the volume.cfg file is as follows: + // create volume-name mount-point + // configure volume-name preference preference-value + // Blank lines and lines starting with the hash character "#" will be ignored. + + ScopedCloseFile fp; + int n = 0; + char line[255]; + const char *filename = "/system/etc/volume.cfg"; + if (!(fp = fopen(filename, "r"))) { + LOG("Unable to open volume configuration file '%s' - ignoring", filename); + return; + } + while(fgets(line, sizeof(line), fp)) { + n++; + + if (line[0] == '#') + continue; + + nsCString commandline(line); + nsCWhitespaceTokenizer tokenizer(commandline); + if (!tokenizer.hasMoreTokens()) { + // Blank line - ignore + continue; + } + + nsCString command(tokenizer.nextToken()); + if (command.EqualsLiteral("create")) { + if (!tokenizer.hasMoreTokens()) { + ERR("No vol_name in %s line %d", filename, n); + continue; + } + nsCString volName(tokenizer.nextToken()); + if (!tokenizer.hasMoreTokens()) { + ERR("No mount point for volume '%s'. %s line %d", + volName.get(), filename, n); + continue; + } + nsCString mountPoint(tokenizer.nextToken()); + RefPtr<Volume> vol = FindAddVolumeByName(volName); + vol->SetFakeVolume(mountPoint); + continue; + } + if (command.EqualsLiteral("configure")) { + if (!tokenizer.hasMoreTokens()) { + ERR("No vol_name in %s line %d", filename, n); + continue; + } + nsCString volName(tokenizer.nextToken()); + if (!tokenizer.hasMoreTokens()) { + ERR("No configuration name specified for volume '%s'. %s line %d", + volName.get(), filename, n); + continue; + } + nsCString configName(tokenizer.nextToken()); + if (!tokenizer.hasMoreTokens()) { + ERR("No value for configuration name '%s'. %s line %d", + configName.get(), filename, n); + continue; + } + nsCString configValue(tokenizer.nextToken()); + RefPtr<Volume> vol = FindVolumeByName(volName); + if (vol) { + vol->SetConfig(configName, configValue); + } else { + ERR("Invalid volume name '%s'.", volName.get()); + } + continue; + } + if (command.EqualsLiteral("ignore")) { + // This command is useful to remove volumes which are being tracked by + // vold, but for which we have no interest. + if (!tokenizer.hasMoreTokens()) { + ERR("No vol_name in %s line %d", filename, n); + continue; + } + nsCString volName(tokenizer.nextToken()); + RemoveVolumeByName(volName); + continue; + } + ERR("Unrecognized command: '%s'", command.get()); + } +} + +void +VolumeManager::DefaultConfig() +{ + + VolumeManager::VolumeArray::size_type numVolumes = VolumeManager::NumVolumes(); + if (numVolumes == 0) { + return; + } + if (numVolumes == 1) { + // This is to cover early shipping phones like the Buri, + // which had no internal storage, and only external sdcard. + // + // Phones line the nexus-4 which only have an internal + // storage area will need to have a volume.cfg file with + // removable set to false. + RefPtr<Volume> vol = VolumeManager::GetVolume(0); + vol->SetIsRemovable(true); + vol->SetIsHotSwappable(true); + return; + } + VolumeManager::VolumeArray::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex); + if (!vol->Name().EqualsLiteral("sdcard")) { + vol->SetIsRemovable(true); + vol->SetIsHotSwappable(true); + } + } +} + +class VolumeListCallback : public VolumeResponseCallback +{ + virtual void ResponseReceived(const VolumeCommand* aCommand) + { + switch (ResponseCode()) { + case ::ResponseCode::VolumeListResult: { + // Each line will look something like: + // + // sdcard /mnt/sdcard 1 + // + // So for each volume that we get back, we update any volumes that + // we have of the same name, or add new ones if they don't exist. + nsCWhitespaceTokenizer tokenizer(ResponseStr()); + nsDependentCSubstring volName(tokenizer.nextToken()); + RefPtr<Volume> vol = VolumeManager::FindAddVolumeByName(volName); + vol->HandleVoldResponse(ResponseCode(), tokenizer); + break; + } + + case ::ResponseCode::CommandOkay: { + // We've received the list of volumes. Now read the Volume.cfg + // file to perform customizations, and then tell everybody + // that we're ready for business. + VolumeManager::DefaultConfig(); + VolumeManager::InitConfig(); + VolumeManager::Dump("READY"); + VolumeManager::SetState(VolumeManager::VOLUMES_READY); + break; + } + } + } +}; + +bool +VolumeManager::OpenSocket() +{ + SetState(STARTING); + if ((mSocket.rwget() = socket_local_client("vold", + ANDROID_SOCKET_NAMESPACE_RESERVED, + SOCK_STREAM)) < 0) { + ERR("Error connecting to vold: (%s) - will retry", strerror(errno)); + return false; + } + // add FD_CLOEXEC flag + int flags = fcntl(mSocket.get(), F_GETFD); + if (flags == -1) { + return false; + } + flags |= FD_CLOEXEC; + if (fcntl(mSocket.get(), F_SETFD, flags) == -1) { + return false; + } + // set non-blocking + if (fcntl(mSocket.get(), F_SETFL, O_NONBLOCK) == -1) { + return false; + } + if (!MessageLoopForIO::current()-> + WatchFileDescriptor(mSocket.get(), + true, + MessageLoopForIO::WATCH_READ, + &mReadWatcher, + this)) { + return false; + } + + LOG("Connected to vold"); + PostCommand(new VolumeListCommand(new VolumeListCallback)); + return true; +} + +//static +void +VolumeManager::PostCommand(VolumeCommand* aCommand) +{ + if (!sVolumeManager) { + ERR("VolumeManager not initialized. Dropping command '%s'", aCommand->Data()); + return; + } + aCommand->SetPending(true); + + DBG("Sending command '%s'", aCommand->Data()); + // vold can only process one command at a time, so add our command + // to the end of the command queue. + sVolumeManager->mCommands.push(aCommand); + if (!sVolumeManager->mCommandPending) { + // There aren't any commands currently being processed, so go + // ahead and kick this one off. + sVolumeManager->mCommandPending = true; + sVolumeManager->WriteCommandData(); + } +} + +/*************************************************************************** +* The WriteCommandData initiates sending of a command to vold. Since +* we're running on the IOThread and not allowed to block, WriteCommandData +* will write as much data as it can, and if not all of the data can be +* written then it will setup a file descriptor watcher and +* OnFileCanWriteWithoutBlocking will call WriteCommandData to write out +* more of the command data. +*/ +void +VolumeManager::WriteCommandData() +{ + if (mCommands.size() == 0) { + return; + } + + VolumeCommand* cmd = mCommands.front(); + if (cmd->BytesRemaining() == 0) { + // All bytes have been written. We're waiting for a response. + return; + } + // There are more bytes left to write. Try to write them all. + ssize_t bytesWritten = write(mSocket.get(), cmd->Data(), cmd->BytesRemaining()); + if (bytesWritten < 0) { + ERR("Failed to write %d bytes to vold socket", cmd->BytesRemaining()); + Restart(); + return; + } + DBG("Wrote %d bytes (of %d)", bytesWritten, cmd->BytesRemaining()); + cmd->ConsumeBytes(bytesWritten); + if (cmd->BytesRemaining() == 0) { + return; + } + // We were unable to write all of the command bytes. Setup a watcher + // so we'll get called again when we can write without blocking. + if (!MessageLoopForIO::current()-> + WatchFileDescriptor(mSocket.get(), + false, // one-shot + MessageLoopForIO::WATCH_WRITE, + &mWriteWatcher, + this)) { + ERR("Failed to setup write watcher for vold socket"); + Restart(); + } +} + +void +VolumeManager::OnLineRead(int aFd, nsDependentCSubstring& aMessage) +{ + MOZ_ASSERT(aFd == mSocket.get()); + char* endPtr; + int responseCode = strtol(aMessage.Data(), &endPtr, 10); + if (*endPtr == ' ') { + endPtr++; + } + + // Now fish out the rest of the line after the response code + nsDependentCString responseLine(endPtr, aMessage.Length() - (endPtr - aMessage.Data())); + DBG("Rcvd: %d '%s'", responseCode, responseLine.Data()); + + if (responseCode >= ::ResponseCode::UnsolicitedInformational) { + // These are unsolicited broadcasts. We intercept these and process + // them ourselves + HandleBroadcast(responseCode, responseLine); + } else { + // Everything else is considered to be part of the command response. + if (mCommands.size() > 0) { + VolumeCommand* cmd = mCommands.front(); + cmd->HandleResponse(responseCode, responseLine); + if (responseCode >= ::ResponseCode::CommandOkay) { + // That's a terminating response. We can remove the command. + mCommands.pop(); + mCommandPending = false; + // Start the next command, if there is one. + WriteCommandData(); + } + } else { + ERR("Response with no command"); + } + } +} + +void +VolumeManager::OnFileCanWriteWithoutBlocking(int aFd) +{ + MOZ_ASSERT(aFd == mSocket.get()); + WriteCommandData(); +} + +void +VolumeManager::HandleBroadcast(int aResponseCode, nsCString& aResponseLine) +{ + // Format of the line is something like: + // + // Volume sdcard /mnt/sdcard state changed from 7 (Shared-Unmounted) to 1 (Idle-Unmounted) + // + // So we parse out the volume name and the state after the string " to " + nsCWhitespaceTokenizer tokenizer(aResponseLine); + tokenizer.nextToken(); // The word "Volume" + nsDependentCSubstring volName(tokenizer.nextToken()); + + RefPtr<Volume> vol = FindVolumeByName(volName); + if (!vol) { + return; + } + vol->HandleVoldResponse(aResponseCode, tokenizer); +} + +void +VolumeManager::Restart() +{ + mReadWatcher.StopWatchingFileDescriptor(); + mWriteWatcher.StopWatchingFileDescriptor(); + + while (!mCommands.empty()) { + mCommands.pop(); + } + mCommandPending = false; + mSocket.dispose(); + Start(); +} + +//static +void +VolumeManager::Start() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + if (!sVolumeManager) { + return; + } + SetState(STARTING); + if (!sVolumeManager->OpenSocket()) { + // Socket open failed, try again in a second. + MessageLoopForIO::current()-> + PostDelayedTask(NewRunnableFunction(VolumeManager::Start), + 1000); + } +} + +void +VolumeManager::OnError() +{ + Restart(); +} + +/***************************************************************************/ + +static void +InitVolumeManagerIOThread() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + MOZ_ASSERT(!sVolumeManager); + + sVolumeManager = new VolumeManager(); + VolumeManager::Start(); + + InitVolumeServiceTestIOThread(); +} + +static void +ShutdownVolumeManagerIOThread() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + sVolumeManager = nullptr; +} + +/************************************************************************** +* +* Public API +* +* Since the VolumeManager runs in IO Thread context, we need to switch +* to IOThread context before we can do anything. +* +**************************************************************************/ + +void +InitVolumeManager() +{ + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(InitVolumeManagerIOThread)); +} + +void +ShutdownVolumeManager() +{ + ShutdownVolumeServiceTest(); + + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(ShutdownVolumeManagerIOThread)); +} + +} // system +} // mozilla diff --git a/dom/system/gonk/VolumeManager.h b/dom/system/gonk/VolumeManager.h new file mode 100644 index 000000000..7c0503389 --- /dev/null +++ b/dom/system/gonk/VolumeManager.h @@ -0,0 +1,192 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_volumemanager_h__ +#define mozilla_system_volumemanager_h__ + +#include <vector> +#include <queue> + +#include "base/message_loop.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Observer.h" +#include "nsISupportsImpl.h" +#include "nsString.h" +#include "nsTArray.h" + +#include "Volume.h" +#include "VolumeCommand.h" + +namespace mozilla { +namespace system { + +/*************************************************************************** +* +* All of the public API mentioned in this file (unless otherwise +* mentioned) must run from the IOThread. +* +***************************************************************************/ + +/*************************************************************************** +* +* The VolumeManager class is a front-end for android's vold service. +* +* Vold uses a unix socket interface and accepts null-terminated string +* commands. The following commands were determined by examining the vold +* source code: +* +* volume list +* volume mount <volname> +* volume unmount <volname> [force] +* volume debug [on|off] +* volume format <volname> +* volume share <volname> <method> +* volume unshare <volname> <method> +* volume shared <volname> <method> +* +* <volname> is the name of the volume as used in /system/etc/vold.fstab +* <method> is ums +* +* dump +* +* share status <method> (Determines if a particular sharing method is available) +* (GB only - not available in ICS) +* +* storage users (??? always crashes vold ???) +* +* asec list +* asec ...lots more... +* +* obb list +* obb ...lots more... +* +* xwarp enable +* xwarp disable +* xwarp status +* +* There is also a command line tool called vdc, which can be used to send +* the above commands to vold. +* +* Currently, only the volume list, share/unshare, and mount/unmount +* commands are being used. +* +***************************************************************************/ + +class VolumeManager final : public MessageLoopForIO::LineWatcher +{ + virtual ~VolumeManager(); + +public: + NS_INLINE_DECL_REFCOUNTING(VolumeManager) + + typedef nsTArray<RefPtr<Volume>> VolumeArray; + + VolumeManager(); + + //----------------------------------------------------------------------- + // + // State related methods. + // + // The VolumeManager starts off in the STARTING state. Once a connection + // is established with vold, it asks for a list of volumes, and once the + // volume list has been received, then the VolumeManager enters the + // VOLUMES_READY state. + // + // If vold crashes, then the VolumeManager will once again enter the + // STARTING state and try to reestablish a connection with vold. + + enum STATE + { + UNINITIALIZED, + STARTING, + VOLUMES_READY + }; + + static STATE State(); + static const char* StateStr(STATE aState); + static const char* StateStr() { return StateStr(State()); } + + class StateChangedEvent + { + public: + StateChangedEvent() {} + }; + + typedef mozilla::Observer<StateChangedEvent> StateObserver; + typedef mozilla::ObserverList<StateChangedEvent> StateObserverList; + + static void RegisterStateObserver(StateObserver* aObserver); + static void UnregisterStateObserver(StateObserver* aObserver); + + //----------------------------------------------------------------------- + + static void Start(); + static void Dump(const char* aLabel); + + static VolumeArray::size_type NumVolumes(); + static already_AddRefed<Volume> GetVolume(VolumeArray::index_type aIndex); + static already_AddRefed<Volume> FindVolumeByName(const nsCSubstring& aName); + static already_AddRefed<Volume> FindAddVolumeByName(const nsCSubstring& aName); + static bool RemoveVolumeByName(const nsCSubstring& aName); + static void InitConfig(); + + static void PostCommand(VolumeCommand* aCommand); + +protected: + + virtual void OnLineRead(int aFd, nsDependentCSubstring& aMessage); + virtual void OnFileCanWriteWithoutBlocking(int aFd); + virtual void OnError(); + + static void DefaultConfig(); + +private: + bool OpenSocket(); + + friend class VolumeListCallback; // Calls SetState + + static void SetState(STATE aNewState); + + void Restart(); + void WriteCommandData(); + void HandleBroadcast(int aResponseCode, nsCString& aResponseLine); + + typedef std::queue<RefPtr<VolumeCommand> > CommandQueue; + + static STATE mState; + static StateObserverList mStateObserverList; + + static const int kRcvBufSize = 1024; + ScopedClose mSocket; + VolumeArray mVolumeArray; + CommandQueue mCommands; + bool mCommandPending; + MessageLoopForIO::FileDescriptorWatcher mReadWatcher; + MessageLoopForIO::FileDescriptorWatcher mWriteWatcher; + RefPtr<VolumeResponseCallback> mBroadcastCallback; +}; + +/*************************************************************************** +* +* The initialization/shutdown functions do not need to be called from +* the IOThread context. +* +***************************************************************************/ + +/** + * Initialize the Volume Manager. On initialization, the VolumeManager will + * attempt to connect with vold and collect the list of volumes that vold + * knows about. + */ +void InitVolumeManager(); + +/** + * Shuts down the Volume Manager. + */ +void ShutdownVolumeManager(); + +} // system +} // mozilla + +#endif // mozilla_system_volumemanager_h__ diff --git a/dom/system/gonk/VolumeManagerLog.h b/dom/system/gonk/VolumeManagerLog.h new file mode 100644 index 000000000..793f4889c --- /dev/null +++ b/dom/system/gonk/VolumeManagerLog.h @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_volumemanagerlog_h__ +#define mozilla_system_volumemanagerlog_h__ + +#undef USE_DEBUG +#define USE_DEBUG 0 + +#if !defined(VOLUME_MANAGER_LOG_TAG) +#define VOLUME_MANAGER_LOG_TAG "VolumeManager" +#endif + +#undef LOG +#undef ERR +#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, VOLUME_MANAGER_LOG_TAG, ## args) +#define ERR(args...) __android_log_print(ANDROID_LOG_ERROR, VOLUME_MANAGER_LOG_TAG, ## args) + +#undef DBG +#if USE_DEBUG +#define DBG(args...) __android_log_print(ANDROID_LOG_DEBUG, VOLUME_MANAGER_LOG_TAG, ## args) +#else +#define DBG(args...) +#endif + +#endif // mozilla_system_volumemanagerlog_h__ diff --git a/dom/system/gonk/VolumeServiceIOThread.cpp b/dom/system/gonk/VolumeServiceIOThread.cpp new file mode 100644 index 000000000..7eda843c0 --- /dev/null +++ b/dom/system/gonk/VolumeServiceIOThread.cpp @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "VolumeServiceIOThread.h" +#include "base/message_loop.h" +#include "nsVolumeService.h" +#include "nsXULAppAPI.h" +#include "Volume.h" +#include "VolumeManager.h" + +namespace mozilla { +namespace system { + +VolumeServiceIOThread::VolumeServiceIOThread(nsVolumeService* aVolumeService) + : mVolumeService(aVolumeService) +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + + VolumeManager::RegisterStateObserver(this); + Volume::RegisterVolumeObserver(this, "VolumeServiceIOThread"); + UpdateAllVolumes(); +} + +VolumeServiceIOThread::~VolumeServiceIOThread() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + Volume::UnregisterVolumeObserver(this, "VolumeServiceIOThread"); + VolumeManager::UnregisterStateObserver(this); +} + +void +VolumeServiceIOThread::Notify(Volume* const & aVolume) +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + if (VolumeManager::State() != VolumeManager::VOLUMES_READY) { + return; + } + mVolumeService->UpdateVolumeIOThread(aVolume); +} + +void +VolumeServiceIOThread::Notify(const VolumeManager::StateChangedEvent& aEvent) +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + UpdateAllVolumes(); +} + +void +VolumeServiceIOThread::UpdateAllVolumes() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + if (VolumeManager::State() != VolumeManager::VOLUMES_READY) { + return; + } + VolumeManager::VolumeArray::size_type numVolumes = VolumeManager::NumVolumes(); + VolumeManager::VolumeArray::index_type volIndex; + + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex); + mVolumeService->UpdateVolumeIOThread(vol); + } +} + +static StaticRefPtr<VolumeServiceIOThread> sVolumeServiceIOThread; + +void +InitVolumeServiceIOThread(nsVolumeService* const & aVolumeService) +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + sVolumeServiceIOThread = new VolumeServiceIOThread(aVolumeService); +} + +void +ShutdownVolumeServiceIOThread() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + sVolumeServiceIOThread = nullptr; +} + +} // system +} // mozilla diff --git a/dom/system/gonk/VolumeServiceIOThread.h b/dom/system/gonk/VolumeServiceIOThread.h new file mode 100644 index 000000000..0c2a6a62f --- /dev/null +++ b/dom/system/gonk/VolumeServiceIOThread.h @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_volumeserviceiothread_h__ +#define mozilla_system_volumeserviceiothread_h__ + +#include "Volume.h" +#include "VolumeManager.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { +namespace system { + +class nsVolumeService; + +/*************************************************************************** +* The nsVolumeServiceIOThread is a companion class to the nsVolumeService +* class, but whose methods are called from IOThread. +*/ +class VolumeServiceIOThread : public VolumeManager::StateObserver, + public Volume::EventObserver +{ + ~VolumeServiceIOThread(); + +public: + NS_INLINE_DECL_REFCOUNTING(VolumeServiceIOThread) + + VolumeServiceIOThread(nsVolumeService* aVolumeService); + +private: + void UpdateAllVolumes(); + + virtual void Notify(const VolumeManager::StateChangedEvent& aEvent); + virtual void Notify(Volume* const & aVolume); + + RefPtr<nsVolumeService> mVolumeService; +}; + +void InitVolumeServiceIOThread(nsVolumeService* const & aVolumeService); +void ShutdownVolumeServiceIOThread(); +void FormatVolume(const nsCString& aVolume); +void MountVolume(const nsCString& aVolume); +void UnmountVolume(const nsCString& aVolume); + +} // system +} // mozilla + +#endif // mozilla_system_volumeserviceiothread_h__ diff --git a/dom/system/gonk/VolumeServiceTest.cpp b/dom/system/gonk/VolumeServiceTest.cpp new file mode 100644 index 000000000..4082e3889 --- /dev/null +++ b/dom/system/gonk/VolumeServiceTest.cpp @@ -0,0 +1,202 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "VolumeServiceTest.h" + +#include "base/message_loop.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsIVolume.h" +#include "nsIVolumeService.h" +#include "nsIVolumeStat.h" +#include "nsXULAppAPI.h" + +#include "mozilla/Services.h" + +#undef VOLUME_MANAGER_LOG_TAG +#define VOLUME_MANAGER_LOG_TAG "VolumeServiceTest" +#include "VolumeManagerLog.h" + +using namespace mozilla::services; + +namespace mozilla { +namespace system { + +#define TEST_NSVOLUME_OBSERVER 0 + +#if TEST_NSVOLUME_OBSERVER + +/*************************************************************************** +* A test class to verify that the Observer stuff is working properly. +*/ +class VolumeTestObserver : public nsIObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + VolumeTestObserver() + { + nsCOMPtr<nsIObserverService> obs = GetObserverService(); + if (!obs) { + return; + } + obs->AddObserver(this, NS_VOLUME_STATE_CHANGED, false); + } + ~VolumeTestObserver() + { + nsCOMPtr<nsIObserverService> obs = GetObserverService(); + if (!obs) { + return; + } + obs->RemoveObserver(this, NS_VOLUME_STATE_CHANGED); + } + + void LogVolume(nsIVolume* vol) + { + nsString volName; + nsString mountPoint; + int32_t volState; + + vol->GetName(volName); + vol->GetMountPoint(mountPoint); + vol->GetState(&volState); + + LOG(" Volume: %s MountPoint: %s State: %s", + NS_LossyConvertUTF16toASCII(volName).get(), + NS_LossyConvertUTF16toASCII(mountPoint).get(), + NS_VolumeStateStr(volState)); + + nsCOMPtr<nsIVolumeStat> stat; + nsresult rv = vol->GetStats(getter_AddRefs(stat)); + if (NS_SUCCEEDED(rv)) { + int64_t totalBytes; + int64_t freeBytes; + + stat->GetTotalBytes(&totalBytes); + stat->GetFreeBytes(&freeBytes); + + LOG(" Total Space: %llu Mb Free Bytes: %llu Mb", + totalBytes / (1024LL * 1024LL), freeBytes / (1024LL * 1024LL)); + } + else { + LOG(" Unable to retrieve stats"); + } + } +}; +static nsCOMPtr<VolumeTestObserver> sTestObserver; + +NS_IMPL_ISUPPORTS(VolumeTestObserver, nsIObserver) + +NS_IMETHODIMP +VolumeTestObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + LOG("TestObserver: topic: %s", aTopic); + + if (strcmp(aTopic, NS_VOLUME_STATE_CHANGED) != 0) { + return NS_OK; + } + nsCOMPtr<nsIVolume> vol = do_QueryInterface(aSubject); + if (vol) { + LogVolume(vol); + } + + // Since this observe method was called then we know that the service + // has been initialized so we can do the VolumeService tests. + + nsCOMPtr<nsIVolumeService> vs = do_GetService(NS_VOLUMESERVICE_CONTRACTID); + if (!vs) { + ERR("do_GetService('%s') failed", NS_VOLUMESERVICE_CONTRACTID); + return NS_ERROR_FAILURE; + } + + nsresult rv = vs->GetVolumeByName(NS_LITERAL_STRING("sdcard"), getter_AddRefs(vol)); + if (NS_SUCCEEDED(rv)) { + LOG("GetVolumeByName( 'sdcard' ) succeeded (expected)"); + LogVolume(vol); + } else { + ERR("GetVolumeByName( 'sdcard' ) failed (unexpected)"); + } + + rv = vs->GetVolumeByName(NS_LITERAL_STRING("foo"), getter_AddRefs(vol)); + if (NS_SUCCEEDED(rv)) { + ERR("GetVolumeByName( 'foo' ) succeeded (unexpected)"); + } else { + LOG("GetVolumeByName( 'foo' ) failed (expected)"); + } + + rv = vs->GetVolumeByPath(NS_LITERAL_STRING("/mnt/sdcard"), getter_AddRefs(vol)); + if (NS_SUCCEEDED(rv)) { + LOG("GetVolumeByPath( '/mnt/sdcard' ) succeeded (expected)"); + LogVolume(vol); + } else { + ERR("GetVolumeByPath( '/mnt/sdcard' ) failed (unexpected"); + } + + rv = vs->GetVolumeByPath(NS_LITERAL_STRING("/mnt/sdcard/foo"), getter_AddRefs(vol)); + if (NS_SUCCEEDED(rv)) { + LOG("GetVolumeByPath( '/mnt/sdcard/foo' ) succeeded (expected)"); + LogVolume(vol); + } else { + LOG("GetVolumeByPath( '/mnt/sdcard/foo' ) failed (unexpected)"); + } + + rv = vs->GetVolumeByPath(NS_LITERAL_STRING("/mnt/sdcardfoo"), getter_AddRefs(vol)); + if (NS_SUCCEEDED(rv)) { + ERR("GetVolumeByPath( '/mnt/sdcardfoo' ) succeeded (unexpected)"); + } else { + LOG("GetVolumeByPath( '/mnt/sdcardfoo' ) failed (expected)"); + } + + return NS_OK; +} + +class InitVolumeServiceTestIO : public Runnable +{ +public: + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + DBG("InitVolumeServiceTest called"); + nsCOMPtr<nsIVolumeService> vs = do_GetService(NS_VOLUMESERVICE_CONTRACTID); + if (!vs) { + ERR("do_GetService('%s') failed", NS_VOLUMESERVICE_CONTRACTID); + return NS_ERROR_FAILURE; + } + sTestObserver = new VolumeTestObserver(); + + return NS_OK; + } +}; +#endif // TEST_NSVOLUME_OBSERVER + +void +InitVolumeServiceTestIOThread() +{ + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + +#if TEST_NSVOLUME_OBSERVER + // Now that the volume manager is initialized we can go + // ahead and do our test (on main thread). + NS_DispatchToMainThread(new InitVolumeServiceTestIO()); +#endif +} + +void +ShutdownVolumeServiceTest() +{ +#if TEST_NSVOLUME_OBSERVER + DBG("ShutdownVolumeServiceTestIOThread called"); + sTestObserver = nullptr; +#endif +} + +} // system +} // mozilla diff --git a/dom/system/gonk/VolumeServiceTest.h b/dom/system/gonk/VolumeServiceTest.h new file mode 100644 index 000000000..71a92bf6c --- /dev/null +++ b/dom/system/gonk/VolumeServiceTest.h @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_volumeservicetest_h__ +#define mozilla_system_volumeservicetest_h__ + + +namespace mozilla { +namespace system { + +void InitVolumeServiceTestIOThread(); +void ShutdownVolumeServiceTest(); + +} // system +} // mozilla + +#endif // mozilla_system_volumeservicetest_h__ + diff --git a/dom/system/gonk/android_audio/AudioSystem.h b/dom/system/gonk/android_audio/AudioSystem.h new file mode 100644 index 000000000..d5841eaaa --- /dev/null +++ b/dom/system/gonk/android_audio/AudioSystem.h @@ -0,0 +1,1134 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_AUDIOSYSTEM_H_ +#define ANDROID_AUDIOSYSTEM_H_ + +#pragma GCC visibility push(default) + +#include <utils/RefBase.h> +#include <utils/threads.h> +#include "IAudioFlinger.h" + +#ifndef VANILLA_ANDROID +/* request to open a direct output with get_output() (by opposition to + * sharing an output with other AudioTracks) + */ +typedef enum { + AUDIO_POLICY_OUTPUT_FLAG_INDIRECT = 0x0, + AUDIO_POLICY_OUTPUT_FLAG_DIRECT = 0x1 +} audio_policy_output_flags_t; + +/* device categories used for audio_policy->set_force_use() */ +typedef enum { + AUDIO_POLICY_FORCE_NONE, + AUDIO_POLICY_FORCE_SPEAKER, + AUDIO_POLICY_FORCE_HEADPHONES, + AUDIO_POLICY_FORCE_BT_SCO, + AUDIO_POLICY_FORCE_BT_A2DP, + AUDIO_POLICY_FORCE_WIRED_ACCESSORY, + AUDIO_POLICY_FORCE_BT_CAR_DOCK, + AUDIO_POLICY_FORCE_BT_DESK_DOCK, + AUDIO_POLICY_FORCE_ANALOG_DOCK, + AUDIO_POLICY_FORCE_DIGITAL_DOCK, + AUDIO_POLICY_FORCE_NO_BT_A2DP, + AUDIO_POLICY_FORCE_CFG_CNT, + AUDIO_POLICY_FORCE_CFG_MAX = AUDIO_POLICY_FORCE_CFG_CNT - 1, + + AUDIO_POLICY_FORCE_DEFAULT = AUDIO_POLICY_FORCE_NONE, +} audio_policy_forced_cfg_t; + +/* usages used for audio_policy->set_force_use() */ +typedef enum { + AUDIO_POLICY_FORCE_FOR_COMMUNICATION, + AUDIO_POLICY_FORCE_FOR_MEDIA, + AUDIO_POLICY_FORCE_FOR_RECORD, + AUDIO_POLICY_FORCE_FOR_DOCK, + + AUDIO_POLICY_FORCE_USE_CNT, + AUDIO_POLICY_FORCE_USE_MAX = AUDIO_POLICY_FORCE_USE_CNT - 1, +} audio_policy_force_use_t; + +typedef enum { + AUDIO_STREAM_DEFAULT = -1, + AUDIO_STREAM_VOICE_CALL = 0, + AUDIO_STREAM_SYSTEM = 1, + AUDIO_STREAM_RING = 2, + AUDIO_STREAM_MUSIC = 3, + AUDIO_STREAM_ALARM = 4, + AUDIO_STREAM_NOTIFICATION = 5, + AUDIO_STREAM_BLUETOOTH_SCO = 6, + AUDIO_STREAM_ENFORCED_AUDIBLE = 7, /* Sounds that cannot be muted by user and must be routed to speaker */ + AUDIO_STREAM_DTMF = 8, + AUDIO_STREAM_TTS = 9, +#if ANDROID_VERSION < 19 + AUDIO_STREAM_FM = 10, +#endif + + AUDIO_STREAM_CNT, + AUDIO_STREAM_MAX = AUDIO_STREAM_CNT - 1, +} audio_stream_type_t; + +/* PCM sub formats */ +typedef enum { + AUDIO_FORMAT_PCM_SUB_16_BIT = 0x1, /* DO NOT CHANGE - PCM signed 16 bits */ + AUDIO_FORMAT_PCM_SUB_8_BIT = 0x2, /* DO NOT CHANGE - PCM unsigned 8 bits */ + AUDIO_FORMAT_PCM_SUB_32_BIT = 0x3, /* PCM signed .31 fixed point */ + AUDIO_FORMAT_PCM_SUB_8_24_BIT = 0x4, /* PCM signed 7.24 fixed point */ +} audio_format_pcm_sub_fmt_t; + +/* Audio format consists in a main format field (upper 8 bits) and a sub format + * field (lower 24 bits). + * + * The main format indicates the main codec type. The sub format field + * indicates options and parameters for each format. The sub format is mainly + * used for record to indicate for instance the requested bitrate or profile. + * It can also be used for certain formats to give informations not present in + * the encoded audio stream (e.g. octet alignement for AMR). + */ +typedef enum { + AUDIO_FORMAT_INVALID = 0xFFFFFFFFUL, + AUDIO_FORMAT_DEFAULT = 0, + AUDIO_FORMAT_PCM = 0x00000000UL, /* DO NOT CHANGE */ + AUDIO_FORMAT_MP3 = 0x01000000UL, + AUDIO_FORMAT_AMR_NB = 0x02000000UL, + AUDIO_FORMAT_AMR_WB = 0x03000000UL, + AUDIO_FORMAT_AAC = 0x04000000UL, + AUDIO_FORMAT_HE_AAC_V1 = 0x05000000UL, + AUDIO_FORMAT_HE_AAC_V2 = 0x06000000UL, + AUDIO_FORMAT_VORBIS = 0x07000000UL, + AUDIO_FORMAT_MAIN_MASK = 0xFF000000UL, + AUDIO_FORMAT_SUB_MASK = 0x00FFFFFFUL, + + /* Aliases */ + AUDIO_FORMAT_PCM_16_BIT = (AUDIO_FORMAT_PCM | + AUDIO_FORMAT_PCM_SUB_16_BIT), + AUDIO_FORMAT_PCM_8_BIT = (AUDIO_FORMAT_PCM | + AUDIO_FORMAT_PCM_SUB_8_BIT), + AUDIO_FORMAT_PCM_32_BIT = (AUDIO_FORMAT_PCM | + AUDIO_FORMAT_PCM_SUB_32_BIT), + AUDIO_FORMAT_PCM_8_24_BIT = (AUDIO_FORMAT_PCM | + AUDIO_FORMAT_PCM_SUB_8_24_BIT), +} audio_format_t; + +typedef enum { + /* output channels */ + AUDIO_CHANNEL_OUT_FRONT_LEFT = 0x1, + AUDIO_CHANNEL_OUT_FRONT_RIGHT = 0x2, + AUDIO_CHANNEL_OUT_FRONT_CENTER = 0x4, + AUDIO_CHANNEL_OUT_LOW_FREQUENCY = 0x8, + AUDIO_CHANNEL_OUT_BACK_LEFT = 0x10, + AUDIO_CHANNEL_OUT_BACK_RIGHT = 0x20, + AUDIO_CHANNEL_OUT_FRONT_LEFT_OF_CENTER = 0x40, + AUDIO_CHANNEL_OUT_FRONT_RIGHT_OF_CENTER = 0x80, + AUDIO_CHANNEL_OUT_BACK_CENTER = 0x100, + AUDIO_CHANNEL_OUT_SIDE_LEFT = 0x200, + AUDIO_CHANNEL_OUT_SIDE_RIGHT = 0x400, + AUDIO_CHANNEL_OUT_TOP_CENTER = 0x800, + AUDIO_CHANNEL_OUT_TOP_FRONT_LEFT = 0x1000, + AUDIO_CHANNEL_OUT_TOP_FRONT_CENTER = 0x2000, + AUDIO_CHANNEL_OUT_TOP_FRONT_RIGHT = 0x4000, + AUDIO_CHANNEL_OUT_TOP_BACK_LEFT = 0x8000, + AUDIO_CHANNEL_OUT_TOP_BACK_CENTER = 0x10000, + AUDIO_CHANNEL_OUT_TOP_BACK_RIGHT = 0x20000, + + AUDIO_CHANNEL_OUT_MONO = AUDIO_CHANNEL_OUT_FRONT_LEFT, + AUDIO_CHANNEL_OUT_STEREO = (AUDIO_CHANNEL_OUT_FRONT_LEFT | + AUDIO_CHANNEL_OUT_FRONT_RIGHT), + AUDIO_CHANNEL_OUT_QUAD = (AUDIO_CHANNEL_OUT_FRONT_LEFT | + AUDIO_CHANNEL_OUT_FRONT_RIGHT | + AUDIO_CHANNEL_OUT_BACK_LEFT | + AUDIO_CHANNEL_OUT_BACK_RIGHT), + AUDIO_CHANNEL_OUT_SURROUND = (AUDIO_CHANNEL_OUT_FRONT_LEFT | + AUDIO_CHANNEL_OUT_FRONT_RIGHT | + AUDIO_CHANNEL_OUT_FRONT_CENTER | + AUDIO_CHANNEL_OUT_BACK_CENTER), + AUDIO_CHANNEL_OUT_5POINT1 = (AUDIO_CHANNEL_OUT_FRONT_LEFT | + AUDIO_CHANNEL_OUT_FRONT_RIGHT | + AUDIO_CHANNEL_OUT_FRONT_CENTER | + AUDIO_CHANNEL_OUT_LOW_FREQUENCY | + AUDIO_CHANNEL_OUT_BACK_LEFT | + AUDIO_CHANNEL_OUT_BACK_RIGHT), + // matches the correct AudioFormat.CHANNEL_OUT_7POINT1_SURROUND definition for 7.1 + AUDIO_CHANNEL_OUT_7POINT1 = (AUDIO_CHANNEL_OUT_FRONT_LEFT | + AUDIO_CHANNEL_OUT_FRONT_RIGHT | + AUDIO_CHANNEL_OUT_FRONT_CENTER | + AUDIO_CHANNEL_OUT_LOW_FREQUENCY | + AUDIO_CHANNEL_OUT_BACK_LEFT | + AUDIO_CHANNEL_OUT_BACK_RIGHT | + AUDIO_CHANNEL_OUT_SIDE_LEFT | + AUDIO_CHANNEL_OUT_SIDE_RIGHT), + AUDIO_CHANNEL_OUT_ALL = (AUDIO_CHANNEL_OUT_FRONT_LEFT | + AUDIO_CHANNEL_OUT_FRONT_RIGHT | + AUDIO_CHANNEL_OUT_FRONT_CENTER | + AUDIO_CHANNEL_OUT_LOW_FREQUENCY | + AUDIO_CHANNEL_OUT_BACK_LEFT | + AUDIO_CHANNEL_OUT_BACK_RIGHT | + AUDIO_CHANNEL_OUT_FRONT_LEFT_OF_CENTER | + AUDIO_CHANNEL_OUT_FRONT_RIGHT_OF_CENTER | + AUDIO_CHANNEL_OUT_BACK_CENTER| + AUDIO_CHANNEL_OUT_SIDE_LEFT| + AUDIO_CHANNEL_OUT_SIDE_RIGHT| + AUDIO_CHANNEL_OUT_TOP_CENTER| + AUDIO_CHANNEL_OUT_TOP_FRONT_LEFT| + AUDIO_CHANNEL_OUT_TOP_FRONT_CENTER| + AUDIO_CHANNEL_OUT_TOP_FRONT_RIGHT| + AUDIO_CHANNEL_OUT_TOP_BACK_LEFT| + AUDIO_CHANNEL_OUT_TOP_BACK_CENTER| + AUDIO_CHANNEL_OUT_TOP_BACK_RIGHT), + + /* input channels */ + AUDIO_CHANNEL_IN_LEFT = 0x4, + AUDIO_CHANNEL_IN_RIGHT = 0x8, + AUDIO_CHANNEL_IN_FRONT = 0x10, + AUDIO_CHANNEL_IN_BACK = 0x20, + AUDIO_CHANNEL_IN_LEFT_PROCESSED = 0x40, + AUDIO_CHANNEL_IN_RIGHT_PROCESSED = 0x80, + AUDIO_CHANNEL_IN_FRONT_PROCESSED = 0x100, + AUDIO_CHANNEL_IN_BACK_PROCESSED = 0x200, + AUDIO_CHANNEL_IN_PRESSURE = 0x400, + AUDIO_CHANNEL_IN_X_AXIS = 0x800, + AUDIO_CHANNEL_IN_Y_AXIS = 0x1000, + AUDIO_CHANNEL_IN_Z_AXIS = 0x2000, + AUDIO_CHANNEL_IN_VOICE_UPLINK = 0x4000, + AUDIO_CHANNEL_IN_VOICE_DNLINK = 0x8000, + + AUDIO_CHANNEL_IN_MONO = AUDIO_CHANNEL_IN_FRONT, + AUDIO_CHANNEL_IN_STEREO = (AUDIO_CHANNEL_IN_LEFT | AUDIO_CHANNEL_IN_RIGHT), + AUDIO_CHANNEL_IN_ALL = (AUDIO_CHANNEL_IN_LEFT | + AUDIO_CHANNEL_IN_RIGHT | + AUDIO_CHANNEL_IN_FRONT | + AUDIO_CHANNEL_IN_BACK| + AUDIO_CHANNEL_IN_LEFT_PROCESSED | + AUDIO_CHANNEL_IN_RIGHT_PROCESSED | + AUDIO_CHANNEL_IN_FRONT_PROCESSED | + AUDIO_CHANNEL_IN_BACK_PROCESSED| + AUDIO_CHANNEL_IN_PRESSURE | + AUDIO_CHANNEL_IN_X_AXIS | + AUDIO_CHANNEL_IN_Y_AXIS | + AUDIO_CHANNEL_IN_Z_AXIS | + AUDIO_CHANNEL_IN_VOICE_UPLINK | + AUDIO_CHANNEL_IN_VOICE_DNLINK), +} audio_channels_t; + +#if ANDROID_VERSION >= 17 +typedef enum { + AUDIO_MODE_INVALID = -2, + AUDIO_MODE_CURRENT = -1, + AUDIO_MODE_NORMAL = 0, + AUDIO_MODE_RINGTONE = 1, + AUDIO_MODE_IN_CALL = 2, + AUDIO_MODE_IN_COMMUNICATION = 3, + + AUDIO_MODE_CNT, + AUDIO_MODE_MAX = AUDIO_MODE_CNT - 1, +} audio_mode_t; +#endif +#endif + +#if ANDROID_VERSION < 17 +typedef enum { + AUDIO_DEVICE_NONE = 0x0, + /* output devices */ + AUDIO_DEVICE_OUT_EARPIECE = 0x1, + AUDIO_DEVICE_OUT_SPEAKER = 0x2, + AUDIO_DEVICE_OUT_WIRED_HEADSET = 0x4, + AUDIO_DEVICE_OUT_WIRED_HEADPHONE = 0x8, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO = 0x10, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET = 0x20, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT = 0x40, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP = 0x80, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES = 0x100, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER = 0x200, + AUDIO_DEVICE_OUT_AUX_DIGITAL = 0x400, + AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET = 0x800, + AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET = 0x1000, + AUDIO_DEVICE_OUT_FM = 0x2000, + AUDIO_DEVICE_OUT_ANC_HEADSET = 0x4000, + AUDIO_DEVICE_OUT_ANC_HEADPHONE = 0x8000, + AUDIO_DEVICE_OUT_FM_TX = 0x10000, + AUDIO_DEVICE_OUT_DIRECTOUTPUT = 0x20000, + AUDIO_DEVICE_OUT_PROXY = 0x40000, + AUDIO_DEVICE_OUT_DEFAULT = 0x80000, + AUDIO_DEVICE_OUT_ALL = (AUDIO_DEVICE_OUT_EARPIECE | + AUDIO_DEVICE_OUT_SPEAKER | + AUDIO_DEVICE_OUT_WIRED_HEADSET | + AUDIO_DEVICE_OUT_WIRED_HEADPHONE | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER | + AUDIO_DEVICE_OUT_AUX_DIGITAL | + AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET | + AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET | + AUDIO_DEVICE_OUT_FM | + AUDIO_DEVICE_OUT_ANC_HEADSET | + AUDIO_DEVICE_OUT_ANC_HEADPHONE | + AUDIO_DEVICE_OUT_FM_TX | + AUDIO_DEVICE_OUT_DIRECTOUTPUT | + AUDIO_DEVICE_OUT_PROXY | + AUDIO_DEVICE_OUT_DEFAULT), + AUDIO_DEVICE_OUT_ALL_A2DP = (AUDIO_DEVICE_OUT_BLUETOOTH_A2DP | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER), + AUDIO_DEVICE_OUT_ALL_SCO = (AUDIO_DEVICE_OUT_BLUETOOTH_SCO | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT), + /* input devices */ + AUDIO_DEVICE_IN_COMMUNICATION = 0x100000, + AUDIO_DEVICE_IN_AMBIENT = 0x200000, + AUDIO_DEVICE_IN_BUILTIN_MIC = 0x400000, + AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET = 0x800000, + AUDIO_DEVICE_IN_WIRED_HEADSET = 0x1000000, + AUDIO_DEVICE_IN_AUX_DIGITAL = 0x2000000, + AUDIO_DEVICE_IN_VOICE_CALL = 0x4000000, + AUDIO_DEVICE_IN_BACK_MIC = 0x8000000, + AUDIO_DEVICE_IN_ANC_HEADSET = 0x10000000, + AUDIO_DEVICE_IN_FM_RX = 0x20000000, + AUDIO_DEVICE_IN_FM_RX_A2DP = 0x40000000, + AUDIO_DEVICE_IN_DEFAULT = 0x80000000, + + AUDIO_DEVICE_IN_ALL = (AUDIO_DEVICE_IN_COMMUNICATION | + AUDIO_DEVICE_IN_AMBIENT | + AUDIO_DEVICE_IN_BUILTIN_MIC | + AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET | + AUDIO_DEVICE_IN_WIRED_HEADSET | + AUDIO_DEVICE_IN_AUX_DIGITAL | + AUDIO_DEVICE_IN_VOICE_CALL | + AUDIO_DEVICE_IN_BACK_MIC | + AUDIO_DEVICE_IN_ANC_HEADSET | + AUDIO_DEVICE_IN_FM_RX | + AUDIO_DEVICE_IN_FM_RX_A2DP | + AUDIO_DEVICE_IN_DEFAULT), + AUDIO_DEVICE_IN_ALL_SCO = AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET, +} audio_devices_t; +#elif ANDROID_VERSION < 21 +enum { + AUDIO_DEVICE_NONE = 0x0, + /* reserved bits */ + AUDIO_DEVICE_BIT_IN = 0x80000000, + AUDIO_DEVICE_BIT_DEFAULT = 0x40000000, + /* output devices */ + AUDIO_DEVICE_OUT_EARPIECE = 0x1, + AUDIO_DEVICE_OUT_SPEAKER = 0x2, + AUDIO_DEVICE_OUT_WIRED_HEADSET = 0x4, + AUDIO_DEVICE_OUT_WIRED_HEADPHONE = 0x8, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO = 0x10, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET = 0x20, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT = 0x40, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP = 0x80, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES = 0x100, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER = 0x200, + AUDIO_DEVICE_OUT_AUX_DIGITAL = 0x400, + AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET = 0x800, + AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET = 0x1000, + AUDIO_DEVICE_OUT_USB_ACCESSORY = 0x2000, + AUDIO_DEVICE_OUT_USB_DEVICE = 0x4000, + AUDIO_DEVICE_OUT_REMOTE_SUBMIX = 0x8000, + AUDIO_DEVICE_OUT_ANC_HEADSET = 0x10000, + AUDIO_DEVICE_OUT_ANC_HEADPHONE = 0x20000, + AUDIO_DEVICE_OUT_PROXY = 0x40000, + AUDIO_DEVICE_OUT_FM = 0x80000, + AUDIO_DEVICE_OUT_FM_TX = 0x100000, + AUDIO_DEVICE_OUT_DEFAULT = AUDIO_DEVICE_BIT_DEFAULT, + AUDIO_DEVICE_OUT_ALL = (AUDIO_DEVICE_OUT_EARPIECE | + AUDIO_DEVICE_OUT_SPEAKER | + AUDIO_DEVICE_OUT_WIRED_HEADSET | + AUDIO_DEVICE_OUT_WIRED_HEADPHONE | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER | + AUDIO_DEVICE_OUT_AUX_DIGITAL | + AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET | + AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET | + AUDIO_DEVICE_OUT_USB_ACCESSORY | + AUDIO_DEVICE_OUT_USB_DEVICE | + AUDIO_DEVICE_OUT_REMOTE_SUBMIX | + AUDIO_DEVICE_OUT_ANC_HEADSET | + AUDIO_DEVICE_OUT_ANC_HEADPHONE | + AUDIO_DEVICE_OUT_PROXY | + AUDIO_DEVICE_OUT_FM | + AUDIO_DEVICE_OUT_FM_TX | + AUDIO_DEVICE_OUT_DEFAULT), + AUDIO_DEVICE_OUT_ALL_A2DP = (AUDIO_DEVICE_OUT_BLUETOOTH_A2DP | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER), + AUDIO_DEVICE_OUT_ALL_SCO = (AUDIO_DEVICE_OUT_BLUETOOTH_SCO | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT), + AUDIO_DEVICE_OUT_ALL_USB = (AUDIO_DEVICE_OUT_USB_ACCESSORY | + AUDIO_DEVICE_OUT_USB_DEVICE), + + /* input devices */ + AUDIO_DEVICE_IN_COMMUNICATION = AUDIO_DEVICE_BIT_IN | 0x1, + AUDIO_DEVICE_IN_AMBIENT = AUDIO_DEVICE_BIT_IN | 0x2, + AUDIO_DEVICE_IN_BUILTIN_MIC = AUDIO_DEVICE_BIT_IN | 0x4, + AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET = AUDIO_DEVICE_BIT_IN | 0x8, + AUDIO_DEVICE_IN_WIRED_HEADSET = AUDIO_DEVICE_BIT_IN | 0x10, + AUDIO_DEVICE_IN_AUX_DIGITAL = AUDIO_DEVICE_BIT_IN | 0x20, + AUDIO_DEVICE_IN_VOICE_CALL = AUDIO_DEVICE_BIT_IN | 0x40, + AUDIO_DEVICE_IN_BACK_MIC = AUDIO_DEVICE_BIT_IN | 0x80, + AUDIO_DEVICE_IN_REMOTE_SUBMIX = AUDIO_DEVICE_BIT_IN | 0x100, + AUDIO_DEVICE_IN_ANLG_DOCK_HEADSET = AUDIO_DEVICE_BIT_IN | 0x200, + AUDIO_DEVICE_IN_DGTL_DOCK_HEADSET = AUDIO_DEVICE_BIT_IN | 0x400, + AUDIO_DEVICE_IN_USB_ACCESSORY = AUDIO_DEVICE_BIT_IN | 0x800, + AUDIO_DEVICE_IN_USB_DEVICE = AUDIO_DEVICE_BIT_IN | 0x1000, + AUDIO_DEVICE_IN_ANC_HEADSET = AUDIO_DEVICE_BIT_IN | 0x2000, + AUDIO_DEVICE_IN_PROXY = AUDIO_DEVICE_BIT_IN | 0x4000, + AUDIO_DEVICE_IN_FM_RX = AUDIO_DEVICE_BIT_IN | 0x8000, + AUDIO_DEVICE_IN_FM_RX_A2DP = AUDIO_DEVICE_BIT_IN | 0x10000, + AUDIO_DEVICE_IN_DEFAULT = AUDIO_DEVICE_BIT_IN | AUDIO_DEVICE_BIT_DEFAULT, + + AUDIO_DEVICE_IN_ALL = (AUDIO_DEVICE_IN_COMMUNICATION | + AUDIO_DEVICE_IN_AMBIENT | + AUDIO_DEVICE_IN_BUILTIN_MIC | + AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET | + AUDIO_DEVICE_IN_WIRED_HEADSET | + AUDIO_DEVICE_IN_AUX_DIGITAL | + AUDIO_DEVICE_IN_VOICE_CALL | + AUDIO_DEVICE_IN_BACK_MIC | + AUDIO_DEVICE_IN_REMOTE_SUBMIX | + AUDIO_DEVICE_IN_ANLG_DOCK_HEADSET | + AUDIO_DEVICE_IN_DGTL_DOCK_HEADSET | + AUDIO_DEVICE_IN_USB_ACCESSORY | + AUDIO_DEVICE_IN_USB_DEVICE | + AUDIO_DEVICE_IN_ANC_HEADSET | + AUDIO_DEVICE_IN_FM_RX | + AUDIO_DEVICE_IN_FM_RX_A2DP | + AUDIO_DEVICE_IN_PROXY | + AUDIO_DEVICE_IN_DEFAULT), + AUDIO_DEVICE_IN_ALL_SCO = AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET, +}; + +typedef uint32_t audio_devices_t; +#else +enum { + AUDIO_DEVICE_NONE = 0x0, + /* reserved bits */ + AUDIO_DEVICE_BIT_IN = 0x80000000, + AUDIO_DEVICE_BIT_DEFAULT = 0x40000000, + /* output devices */ + AUDIO_DEVICE_OUT_EARPIECE = 0x1, + AUDIO_DEVICE_OUT_SPEAKER = 0x2, + AUDIO_DEVICE_OUT_WIRED_HEADSET = 0x4, + AUDIO_DEVICE_OUT_WIRED_HEADPHONE = 0x8, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO = 0x10, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET = 0x20, + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT = 0x40, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP = 0x80, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES = 0x100, + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER = 0x200, + AUDIO_DEVICE_OUT_AUX_DIGITAL = 0x400, + AUDIO_DEVICE_OUT_HDMI = AUDIO_DEVICE_OUT_AUX_DIGITAL, + /* uses an analog connection (multiplexed over the USB connector pins for instance) */ + AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET = 0x800, + AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET = 0x1000, + /* USB accessory mode: your Android device is a USB device and the dock is a USB host */ + AUDIO_DEVICE_OUT_USB_ACCESSORY = 0x2000, + /* USB host mode: your Android device is a USB host and the dock is a USB device */ + AUDIO_DEVICE_OUT_USB_DEVICE = 0x4000, + AUDIO_DEVICE_OUT_REMOTE_SUBMIX = 0x8000, + /* Telephony voice TX path */ + AUDIO_DEVICE_OUT_TELEPHONY_TX = 0x10000, + /* Analog jack with line impedance detected */ + AUDIO_DEVICE_OUT_LINE = 0x20000, + /* HDMI Audio Return Channel */ + AUDIO_DEVICE_OUT_HDMI_ARC = 0x40000, + /* S/PDIF out */ + AUDIO_DEVICE_OUT_SPDIF = 0x80000, + /* FM transmitter out */ + AUDIO_DEVICE_OUT_FM = 0x100000, + /* Line out for av devices */ + AUDIO_DEVICE_OUT_AUX_LINE = 0x200000, + /* limited-output speaker device for acoustic safety */ + AUDIO_DEVICE_OUT_SPEAKER_SAFE = 0x400000, + AUDIO_DEVICE_OUT_DEFAULT = AUDIO_DEVICE_BIT_DEFAULT, + AUDIO_DEVICE_OUT_ALL = (AUDIO_DEVICE_OUT_EARPIECE | + AUDIO_DEVICE_OUT_SPEAKER | + AUDIO_DEVICE_OUT_WIRED_HEADSET | + AUDIO_DEVICE_OUT_WIRED_HEADPHONE | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER | + AUDIO_DEVICE_OUT_AUX_DIGITAL | + AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET | + AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET | + AUDIO_DEVICE_OUT_USB_ACCESSORY | + AUDIO_DEVICE_OUT_USB_DEVICE | + AUDIO_DEVICE_OUT_REMOTE_SUBMIX | + AUDIO_DEVICE_OUT_TELEPHONY_TX | + AUDIO_DEVICE_OUT_LINE | + AUDIO_DEVICE_OUT_HDMI_ARC | + AUDIO_DEVICE_OUT_SPDIF | + AUDIO_DEVICE_OUT_FM | + AUDIO_DEVICE_OUT_AUX_LINE | + AUDIO_DEVICE_OUT_SPEAKER_SAFE | + AUDIO_DEVICE_OUT_DEFAULT), + AUDIO_DEVICE_OUT_ALL_A2DP = (AUDIO_DEVICE_OUT_BLUETOOTH_A2DP | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER), + AUDIO_DEVICE_OUT_ALL_SCO = (AUDIO_DEVICE_OUT_BLUETOOTH_SCO | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET | + AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT), + AUDIO_DEVICE_OUT_ALL_USB = (AUDIO_DEVICE_OUT_USB_ACCESSORY | + AUDIO_DEVICE_OUT_USB_DEVICE), + /* input devices */ + AUDIO_DEVICE_IN_COMMUNICATION = AUDIO_DEVICE_BIT_IN | 0x1, + AUDIO_DEVICE_IN_AMBIENT = AUDIO_DEVICE_BIT_IN | 0x2, + AUDIO_DEVICE_IN_BUILTIN_MIC = AUDIO_DEVICE_BIT_IN | 0x4, + AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET = AUDIO_DEVICE_BIT_IN | 0x8, + AUDIO_DEVICE_IN_WIRED_HEADSET = AUDIO_DEVICE_BIT_IN | 0x10, + AUDIO_DEVICE_IN_AUX_DIGITAL = AUDIO_DEVICE_BIT_IN | 0x20, + AUDIO_DEVICE_IN_HDMI = AUDIO_DEVICE_IN_AUX_DIGITAL, + /* Telephony voice RX path */ + AUDIO_DEVICE_IN_VOICE_CALL = AUDIO_DEVICE_BIT_IN | 0x40, + AUDIO_DEVICE_IN_BACK_MIC = AUDIO_DEVICE_BIT_IN | 0x80, + AUDIO_DEVICE_IN_REMOTE_SUBMIX = AUDIO_DEVICE_BIT_IN | 0x100, + AUDIO_DEVICE_IN_ANLG_DOCK_HEADSET = AUDIO_DEVICE_BIT_IN | 0x200, + AUDIO_DEVICE_IN_DGTL_DOCK_HEADSET = AUDIO_DEVICE_BIT_IN | 0x400, + AUDIO_DEVICE_IN_USB_ACCESSORY = AUDIO_DEVICE_BIT_IN | 0x800, + AUDIO_DEVICE_IN_USB_DEVICE = AUDIO_DEVICE_BIT_IN | 0x1000, + /* FM tuner input */ + AUDIO_DEVICE_IN_FM_TUNER = AUDIO_DEVICE_BIT_IN | 0x2000, + /* TV tuner input */ + AUDIO_DEVICE_IN_TV_TUNER = AUDIO_DEVICE_BIT_IN | 0x4000, + /* Analog jack with line impedance detected */ + AUDIO_DEVICE_IN_LINE = AUDIO_DEVICE_BIT_IN | 0x8000, + /* S/PDIF in */ + AUDIO_DEVICE_IN_SPDIF = AUDIO_DEVICE_BIT_IN | 0x10000, + AUDIO_DEVICE_IN_BLUETOOTH_A2DP = AUDIO_DEVICE_BIT_IN | 0x20000, + AUDIO_DEVICE_IN_LOOPBACK = AUDIO_DEVICE_BIT_IN | 0x40000, + AUDIO_DEVICE_IN_DEFAULT = AUDIO_DEVICE_BIT_IN | AUDIO_DEVICE_BIT_DEFAULT, + AUDIO_DEVICE_IN_ALL = (AUDIO_DEVICE_IN_COMMUNICATION | + AUDIO_DEVICE_IN_AMBIENT | + AUDIO_DEVICE_IN_BUILTIN_MIC | + AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET | + AUDIO_DEVICE_IN_WIRED_HEADSET | + AUDIO_DEVICE_IN_AUX_DIGITAL | + AUDIO_DEVICE_IN_VOICE_CALL | + AUDIO_DEVICE_IN_BACK_MIC | + AUDIO_DEVICE_IN_REMOTE_SUBMIX | + AUDIO_DEVICE_IN_ANLG_DOCK_HEADSET | + AUDIO_DEVICE_IN_DGTL_DOCK_HEADSET | + AUDIO_DEVICE_IN_USB_ACCESSORY | + AUDIO_DEVICE_IN_USB_DEVICE | + AUDIO_DEVICE_IN_FM_TUNER | + AUDIO_DEVICE_IN_TV_TUNER | + AUDIO_DEVICE_IN_LINE | + AUDIO_DEVICE_IN_SPDIF | + AUDIO_DEVICE_IN_BLUETOOTH_A2DP | + AUDIO_DEVICE_IN_LOOPBACK | + AUDIO_DEVICE_IN_DEFAULT), + AUDIO_DEVICE_IN_ALL_SCO = AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET, + AUDIO_DEVICE_IN_ALL_USB = (AUDIO_DEVICE_IN_USB_ACCESSORY | + AUDIO_DEVICE_IN_USB_DEVICE), +}; + +typedef uint32_t audio_devices_t; +#endif + +static inline bool audio_is_output_device(uint32_t device) +{ +#if ANDROID_VERSION < 17 + if ((__builtin_popcount(device) == 1) && ((device & ~AUDIO_DEVICE_OUT_ALL) == 0)) + return true; + else + return false; +#else + if (((device & AUDIO_DEVICE_BIT_IN) == 0) && + (__builtin_popcount(device) == 1) && ((device & ~AUDIO_DEVICE_OUT_ALL) == 0)) + return true; + else + return false; +#endif +} + +/* device connection states used for audio_policy->set_device_connection_state() + * */ +typedef enum { + AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE, + AUDIO_POLICY_DEVICE_STATE_AVAILABLE, + + AUDIO_POLICY_DEVICE_STATE_CNT, + AUDIO_POLICY_DEVICE_STATE_MAX = AUDIO_POLICY_DEVICE_STATE_CNT - 1, +} audio_policy_dev_state_t; + +namespace android { + +typedef void (*audio_error_callback)(status_t err); +typedef int audio_io_handle_t; + +class IAudioPolicyService; +class String8; + +class AudioSystem +{ +public: + + enum stream_type { + DEFAULT =-1, + VOICE_CALL = 0, + SYSTEM = 1, + RING = 2, + MUSIC = 3, + ALARM = 4, + NOTIFICATION = 5, + BLUETOOTH_SCO = 6, + ENFORCED_AUDIBLE = 7, // Sounds that cannot be muted by user and must be routed to speaker + DTMF = 8, + TTS = 9, + FM = 10, + NUM_STREAM_TYPES + }; + + // Audio sub formats (see AudioSystem::audio_format). + enum pcm_sub_format { + PCM_SUB_16_BIT = 0x1, // must be 1 for backward compatibility + PCM_SUB_8_BIT = 0x2, // must be 2 for backward compatibility + }; + + // MP3 sub format field definition : can use 11 LSBs in the same way as MP3 frame header to specify + // bit rate, stereo mode, version... + enum mp3_sub_format { + //TODO + }; + + // AMR NB/WB sub format field definition: specify frame block interleaving, bandwidth efficient or octet aligned, + // encoding mode for recording... + enum amr_sub_format { + //TODO + }; + + // AAC sub format field definition: specify profile or bitrate for recording... + enum aac_sub_format { + //TODO + }; + + // VORBIS sub format field definition: specify quality for recording... + enum vorbis_sub_format { + //TODO + }; + + // Audio format consists in a main format field (upper 8 bits) and a sub format field (lower 24 bits). + // The main format indicates the main codec type. The sub format field indicates options and parameters + // for each format. The sub format is mainly used for record to indicate for instance the requested bitrate + // or profile. It can also be used for certain formats to give informations not present in the encoded + // audio stream (e.g. octet alignement for AMR). + enum audio_format { + INVALID_FORMAT = -1, + FORMAT_DEFAULT = 0, + PCM = 0x00000000, // must be 0 for backward compatibility + MP3 = 0x01000000, + AMR_NB = 0x02000000, + AMR_WB = 0x03000000, + AAC = 0x04000000, + HE_AAC_V1 = 0x05000000, + HE_AAC_V2 = 0x06000000, + VORBIS = 0x07000000, + EVRC = 0x08000000, + QCELP = 0x09000000, + VOIP_PCM_INPUT = 0x0A000000, + MAIN_FORMAT_MASK = 0xFF000000, + SUB_FORMAT_MASK = 0x00FFFFFF, + // Aliases + PCM_16_BIT = (PCM|PCM_SUB_16_BIT), + PCM_8_BIT = (PCM|PCM_SUB_8_BIT) + }; + + + // Channel mask definitions must be kept in sync with JAVA values in /media/java/android/media/AudioFormat.java + enum audio_channels { + // output channels + CHANNEL_OUT_FRONT_LEFT = 0x4, + CHANNEL_OUT_FRONT_RIGHT = 0x8, + CHANNEL_OUT_FRONT_CENTER = 0x10, + CHANNEL_OUT_LOW_FREQUENCY = 0x20, + CHANNEL_OUT_BACK_LEFT = 0x40, + CHANNEL_OUT_BACK_RIGHT = 0x80, + CHANNEL_OUT_FRONT_LEFT_OF_CENTER = 0x100, + CHANNEL_OUT_FRONT_RIGHT_OF_CENTER = 0x200, + CHANNEL_OUT_BACK_CENTER = 0x400, + CHANNEL_OUT_MONO = CHANNEL_OUT_FRONT_LEFT, + CHANNEL_OUT_STEREO = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT), + CHANNEL_OUT_QUAD = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT | + CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT), + CHANNEL_OUT_SURROUND = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT | + CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_BACK_CENTER), + CHANNEL_OUT_5POINT1 = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT | + CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_LOW_FREQUENCY | CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT), + CHANNEL_OUT_7POINT1 = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT | + CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_LOW_FREQUENCY | CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT | + CHANNEL_OUT_FRONT_LEFT_OF_CENTER | CHANNEL_OUT_FRONT_RIGHT_OF_CENTER), + CHANNEL_OUT_ALL = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT | + CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_LOW_FREQUENCY | CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT | + CHANNEL_OUT_FRONT_LEFT_OF_CENTER | CHANNEL_OUT_FRONT_RIGHT_OF_CENTER | CHANNEL_OUT_BACK_CENTER), + + // input channels + CHANNEL_IN_LEFT = 0x4, + CHANNEL_IN_RIGHT = 0x8, + CHANNEL_IN_FRONT = 0x10, + CHANNEL_IN_BACK = 0x20, + CHANNEL_IN_LEFT_PROCESSED = 0x40, + CHANNEL_IN_RIGHT_PROCESSED = 0x80, + CHANNEL_IN_FRONT_PROCESSED = 0x100, + CHANNEL_IN_BACK_PROCESSED = 0x200, + CHANNEL_IN_PRESSURE = 0x400, + CHANNEL_IN_X_AXIS = 0x800, + CHANNEL_IN_Y_AXIS = 0x1000, + CHANNEL_IN_Z_AXIS = 0x2000, + CHANNEL_IN_VOICE_UPLINK = 0x4000, + CHANNEL_IN_VOICE_DNLINK = 0x8000, + CHANNEL_IN_MONO = CHANNEL_IN_FRONT, + CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT), + CHANNEL_IN_ALL = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT | CHANNEL_IN_FRONT | CHANNEL_IN_BACK| + CHANNEL_IN_LEFT_PROCESSED | CHANNEL_IN_RIGHT_PROCESSED | CHANNEL_IN_FRONT_PROCESSED | CHANNEL_IN_BACK_PROCESSED| + CHANNEL_IN_PRESSURE | CHANNEL_IN_X_AXIS | CHANNEL_IN_Y_AXIS | CHANNEL_IN_Z_AXIS | + CHANNEL_IN_VOICE_UPLINK | CHANNEL_IN_VOICE_DNLINK) + }; + + enum audio_mode { + MODE_INVALID = -2, + MODE_CURRENT = -1, + MODE_NORMAL = 0, + MODE_RINGTONE, + MODE_IN_CALL, + MODE_IN_COMMUNICATION, + NUM_MODES // not a valid entry, denotes end-of-list + }; + + enum audio_in_acoustics { + AGC_ENABLE = 0x0001, + AGC_DISABLE = 0, + NS_ENABLE = 0x0002, + NS_DISABLE = 0, + TX_IIR_ENABLE = 0x0004, + TX_DISABLE = 0 + }; + + // special audio session values + enum audio_sessions { + SESSION_OUTPUT_STAGE = -1, // session for effects attached to a particular output stream + // (value must be less than 0) + SESSION_OUTPUT_MIX = 0, // session for effects applied to output mix. These effects can + // be moved by audio policy manager to another output stream + // (value must be 0) + }; + + /* These are static methods to control the system-wide AudioFlinger + * only privileged processes can have access to them + */ + + // mute/unmute microphone + static status_t muteMicrophone(bool state); + static status_t isMicrophoneMuted(bool *state); + + // set/get master volume + static status_t setMasterVolume(float value); + static status_t getMasterVolume(float* volume); + // mute/unmute audio outputs + static status_t setMasterMute(bool mute); + static status_t getMasterMute(bool* mute); + + // set/get stream volume on specified output + static status_t setStreamVolume(int stream, float value, int output); + static status_t getStreamVolume(int stream, float* volume, int output); + + // mute/unmute stream + static status_t setStreamMute(int stream, bool mute); + static status_t getStreamMute(int stream, bool* mute); + + // set audio mode in audio hardware (see AudioSystem::audio_mode) + static status_t setMode(int mode); + + // returns true in *state if tracks are active on the specified stream + static status_t isStreamActive(int stream, bool *state); + + // set/get audio hardware parameters. The function accepts a list of parameters + // key value pairs in the form: key1=value1;key2=value2;... + // Some keys are reserved for standard parameters (See AudioParameter class). + static status_t setParameters(audio_io_handle_t ioHandle, const String8& keyValuePairs); + static String8 getParameters(audio_io_handle_t ioHandle, const String8& keys); + + static void setErrorCallback(audio_error_callback cb); + + // helper function to obtain AudioFlinger service handle + static const sp<IAudioFlinger>& get_audio_flinger(); + + static float linearToLog(int volume); + static int logToLinear(float volume); + + static status_t getOutputSamplingRate(int* samplingRate, int stream = DEFAULT); + static status_t getOutputFrameCount(int* frameCount, int stream = DEFAULT); + static status_t getOutputLatency(uint32_t* latency, int stream = DEFAULT); + + static bool routedToA2dpOutput(int streamType); + + static status_t getInputBufferSize(uint32_t sampleRate, int format, int channelCount, + size_t* buffSize); + + static status_t setVoiceVolume(float volume); + + // return the number of audio frames written by AudioFlinger to audio HAL and + // audio dsp to DAC since the output on which the specificed stream is playing + // has exited standby. + // returned status (from utils/Errors.h) can be: + // - NO_ERROR: successful operation, halFrames and dspFrames point to valid data + // - INVALID_OPERATION: Not supported on current hardware platform + // - BAD_VALUE: invalid parameter + // NOTE: this feature is not supported on all hardware platforms and it is + // necessary to check returned status before using the returned values. + static status_t getRenderPosition(uint32_t *halFrames, uint32_t *dspFrames, int stream = DEFAULT); + + static unsigned int getInputFramesLost(audio_io_handle_t ioHandle); + + static int newAudioSessionId(); + // + // AudioPolicyService interface + // + + enum audio_devices { + // output devices + DEVICE_OUT_EARPIECE = 0x1, + DEVICE_OUT_SPEAKER = 0x2, + DEVICE_OUT_WIRED_HEADSET = 0x4, + DEVICE_OUT_WIRED_HEADPHONE = 0x8, + DEVICE_OUT_BLUETOOTH_SCO = 0x10, + DEVICE_OUT_BLUETOOTH_SCO_HEADSET = 0x20, + DEVICE_OUT_BLUETOOTH_SCO_CARKIT = 0x40, + DEVICE_OUT_BLUETOOTH_A2DP = 0x80, + DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES = 0x100, + DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER = 0x200, + DEVICE_OUT_AUX_DIGITAL = 0x400, + DEVICE_OUT_DEFAULT = 0x8000, + DEVICE_OUT_ALL = (DEVICE_OUT_EARPIECE | DEVICE_OUT_SPEAKER | DEVICE_OUT_WIRED_HEADSET | + DEVICE_OUT_WIRED_HEADPHONE | DEVICE_OUT_BLUETOOTH_SCO | DEVICE_OUT_BLUETOOTH_SCO_HEADSET | + DEVICE_OUT_BLUETOOTH_SCO_CARKIT | DEVICE_OUT_BLUETOOTH_A2DP | DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER | DEVICE_OUT_AUX_DIGITAL | DEVICE_OUT_DEFAULT), + DEVICE_OUT_ALL_A2DP = (DEVICE_OUT_BLUETOOTH_A2DP | DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | + DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER), + + // input devices + DEVICE_IN_COMMUNICATION = 0x10000, + DEVICE_IN_AMBIENT = 0x20000, + DEVICE_IN_BUILTIN_MIC = 0x40000, + DEVICE_IN_BLUETOOTH_SCO_HEADSET = 0x80000, + DEVICE_IN_WIRED_HEADSET = 0x100000, + DEVICE_IN_AUX_DIGITAL = 0x200000, + DEVICE_IN_VOICE_CALL = 0x400000, + DEVICE_IN_BACK_MIC = 0x800000, + DEVICE_IN_DEFAULT = 0x80000000, + + DEVICE_IN_ALL = (DEVICE_IN_COMMUNICATION | DEVICE_IN_AMBIENT | DEVICE_IN_BUILTIN_MIC | + DEVICE_IN_BLUETOOTH_SCO_HEADSET | DEVICE_IN_WIRED_HEADSET | DEVICE_IN_AUX_DIGITAL | + DEVICE_IN_VOICE_CALL | DEVICE_IN_BACK_MIC | DEVICE_IN_DEFAULT) + }; + + // device connection states used for setDeviceConnectionState() + enum device_connection_state { + DEVICE_STATE_UNAVAILABLE, + DEVICE_STATE_AVAILABLE, + NUM_DEVICE_STATES + }; + + // request to open a direct output with getOutput() (by opposition to sharing an output with other AudioTracks) + enum output_flags { + OUTPUT_FLAG_INDIRECT = 0x0, + OUTPUT_FLAG_DIRECT = 0x1 + }; + + // device categories used for setForceUse() + enum forced_config { + FORCE_NONE, + FORCE_SPEAKER, + FORCE_HEADPHONES, + FORCE_BT_SCO, + FORCE_BT_A2DP, + FORCE_WIRED_ACCESSORY, + FORCE_BT_CAR_DOCK, + FORCE_BT_DESK_DOCK, + FORCE_ANALOG_DOCK, + FORCE_DIGITAL_DOCK, + FORCE_NO_BT_A2DP, + NUM_FORCE_CONFIG, + FORCE_DEFAULT = FORCE_NONE + }; + + // usages used for setForceUse() + enum force_use { + FOR_COMMUNICATION, + FOR_MEDIA, + FOR_RECORD, + FOR_DOCK, + NUM_FORCE_USE + }; + + // types of io configuration change events received with ioConfigChanged() + enum io_config_event { + OUTPUT_OPENED, + OUTPUT_CLOSED, + OUTPUT_CONFIG_CHANGED, + INPUT_OPENED, + INPUT_CLOSED, + INPUT_CONFIG_CHANGED, + STREAM_CONFIG_CHANGED, + NUM_CONFIG_EVENTS + }; + + // audio output descritor used to cache output configurations in client process to avoid frequent calls + // through IAudioFlinger + class OutputDescriptor { + public: + OutputDescriptor() + : samplingRate(0), format(0), channels(0), frameCount(0), latency(0) {} + + uint32_t samplingRate; + int32_t format; + int32_t channels; + size_t frameCount; + uint32_t latency; + }; + + // + // IAudioPolicyService interface (see AudioPolicyInterface for method descriptions) + // + static status_t setDeviceConnectionState(audio_devices device, device_connection_state state, const char *device_address); + static device_connection_state getDeviceConnectionState(audio_devices device, const char *device_address); + static status_t setPhoneState(int state); +#if ANDROID_VERSION >= 17 + static status_t setPhoneState(audio_mode_t state); +#endif + static status_t setRingerMode(uint32_t mode, uint32_t mask); +#ifdef VANILLA_ANDROID + static status_t setForceUse(force_use usage, forced_config config); + static forced_config getForceUse(force_use usage); + static audio_io_handle_t getOutput(stream_type stream, + uint32_t samplingRate = 0, + uint32_t format = FORMAT_DEFAULT, + uint32_t channels = CHANNEL_OUT_STEREO, + output_flags flags = OUTPUT_FLAG_INDIRECT); + static status_t setDeviceConnectionState(audio_devices_t device, audio_policy_dev_state_t state, const char *device_address); + static status_t setFmVolume(float volume); + static audio_policy_dev_state_t getDeviceConnectionState(audio_devices_t device, const char *device_address); +#else + static status_t setForceUse(force_use usage, forced_config config) __attribute__((weak)); + static forced_config getForceUse(force_use usage) __attribute__((weak)); + static audio_io_handle_t getOutput(stream_type stream, + uint32_t samplingRate = 0, + uint32_t format = FORMAT_DEFAULT, + uint32_t channels = CHANNEL_OUT_STEREO, + output_flags flags = OUTPUT_FLAG_INDIRECT) __attribute__((weak)); + + static status_t setForceUse(audio_policy_force_use_t usage, audio_policy_forced_cfg_t config) __attribute__((weak)); + static audio_policy_forced_cfg_t getForceUse(audio_policy_force_use_t usage) __attribute__((weak)); + static audio_io_handle_t getOutput(audio_stream_type_t stream, + uint32_t samplingRate = 0, + uint32_t format = AUDIO_FORMAT_DEFAULT, + uint32_t channels = AUDIO_CHANNEL_OUT_STEREO, + audio_policy_output_flags_t flags = AUDIO_POLICY_OUTPUT_FLAG_INDIRECT) __attribute__((weak)); + static status_t setDeviceConnectionState(audio_devices_t device, audio_policy_dev_state_t state, const char *device_address) __attribute__((weak)); + static status_t setFmVolume(float volume) __attribute__((weak)); + static audio_policy_dev_state_t getDeviceConnectionState(audio_devices_t device, const char *device_address) __attribute__((weak)); + +#endif + static status_t startOutput(audio_io_handle_t output, + AudioSystem::stream_type stream, + int session = 0); + static status_t stopOutput(audio_io_handle_t output, + AudioSystem::stream_type stream, + int session = 0); + static void releaseOutput(audio_io_handle_t output); + static audio_io_handle_t getInput(int inputSource, + uint32_t samplingRate = 0, + uint32_t format = FORMAT_DEFAULT, + uint32_t channels = CHANNEL_IN_MONO, + audio_in_acoustics acoustics = (audio_in_acoustics)0); + static status_t startInput(audio_io_handle_t input); + static status_t stopInput(audio_io_handle_t input); + static void releaseInput(audio_io_handle_t input); + static status_t initStreamVolume(stream_type stream, + int indexMin, + int indexMax); + static status_t initStreamVolume(audio_stream_type_t stream, + int indexMin, + int indexMax); + static status_t setStreamVolumeIndex(stream_type stream, int index); + static status_t setStreamVolumeIndex(audio_stream_type_t stream, int index); +#if ANDROID_VERSION >= 17 + static status_t setStreamVolumeIndex(audio_stream_type_t stream, + int index, + audio_devices_t device); + static status_t getStreamVolumeIndex(audio_stream_type_t stream, + int *index, + audio_devices_t device); +#endif + static status_t getStreamVolumeIndex(stream_type stream, int *index); + static status_t getStreamVolumeIndex(audio_stream_type_t stream, int *index); + + static uint32_t getStrategyForStream(stream_type stream); +#if ANDROID_VERSION >= 17 + static audio_devices_t getDevicesForStream(audio_stream_type_t stream); +#endif + + static audio_io_handle_t getOutputForEffect(effect_descriptor_t *desc); + static status_t registerEffect(effect_descriptor_t *desc, + audio_io_handle_t output, + uint32_t strategy, + int session, + int id); + static status_t unregisterEffect(int id); + + static const sp<IAudioPolicyService>& get_audio_policy_service(); + + // ---------------------------------------------------------------------------- + + static uint32_t popCount(uint32_t u); + static bool isOutputDevice(audio_devices device); + static bool isInputDevice(audio_devices device); + static bool isA2dpDevice(audio_devices device); + static bool isBluetoothScoDevice(audio_devices device); + static bool isSeperatedStream(stream_type stream); + static bool isLowVisibility(stream_type stream); + static bool isOutputChannel(uint32_t channel); + static bool isInputChannel(uint32_t channel); + static bool isValidFormat(uint32_t format); + static bool isLinearPCM(uint32_t format); + static bool isModeInCall(); + +#if ANDROID_VERSION >= 21 + class AudioPortCallback : public RefBase + { + public: + + AudioPortCallback() {} + virtual ~AudioPortCallback() {} + + virtual void onAudioPortListUpdate() = 0; + virtual void onAudioPatchListUpdate() = 0; + virtual void onServiceDied() = 0; + + }; + + static void setAudioPortCallback(sp<AudioPortCallback> callBack); +#endif + +private: + + class AudioFlingerClient: public IBinder::DeathRecipient, public BnAudioFlingerClient + { + public: + AudioFlingerClient() { + } + + // DeathRecipient + virtual void binderDied(const wp<IBinder>& who); + + // IAudioFlingerClient + + // indicate a change in the configuration of an output or input: keeps the cached + // values for output/input parameters upto date in client process + virtual void ioConfigChanged(int event, int ioHandle, void *param2); + }; + + class AudioPolicyServiceClient: public IBinder::DeathRecipient + { + public: + AudioPolicyServiceClient() { + } + + // DeathRecipient + virtual void binderDied(const wp<IBinder>& who); + }; + + static sp<AudioFlingerClient> gAudioFlingerClient; + static sp<AudioPolicyServiceClient> gAudioPolicyServiceClient; + friend class AudioFlingerClient; + friend class AudioPolicyServiceClient; + + static Mutex gLock; + static sp<IAudioFlinger> gAudioFlinger; + static audio_error_callback gAudioErrorCallback; + + static size_t gInBuffSize; + // previous parameters for recording buffer size queries + static uint32_t gPrevInSamplingRate; + static int gPrevInFormat; + static int gPrevInChannelCount; + + static sp<IAudioPolicyService> gAudioPolicyService; + + // mapping between stream types and outputs + static DefaultKeyedVector<int, audio_io_handle_t> gStreamOutputMap; + // list of output descritor containing cached parameters (sampling rate, framecount, channel count...) + static DefaultKeyedVector<audio_io_handle_t, OutputDescriptor *> gOutputs; +}; + +class AudioParameter { + +public: + AudioParameter() {} + AudioParameter(const String8& keyValuePairs); + virtual ~AudioParameter(); + + // reserved parameter keys for changing standard parameters with setParameters() function. + // Using these keys is mandatory for AudioFlinger to properly monitor audio output/input + // configuration changes and act accordingly. + // keyRouting: to change audio routing, value is an int in AudioSystem::audio_devices + // keySamplingRate: to change sampling rate routing, value is an int + // keyFormat: to change audio format, value is an int in AudioSystem::audio_format + // keyChannels: to change audio channel configuration, value is an int in AudioSystem::audio_channels + // keyFrameCount: to change audio output frame count, value is an int + // keyInputSource: to change audio input source, value is an int in audio_source + // (defined in media/mediarecorder.h) + static const char *keyRouting; + static const char *keySamplingRate; + static const char *keyFormat; + static const char *keyChannels; + static const char *keyFrameCount; + static const char *keyInputSource; + + String8 toString(); + + status_t add(const String8& key, const String8& value); + status_t addInt(const String8& key, const int value); + status_t addFloat(const String8& key, const float value); + + status_t remove(const String8& key); + + status_t get(const String8& key, String8& value); + status_t getInt(const String8& key, int& value); + status_t getFloat(const String8& key, float& value); + status_t getAt(size_t index, String8& key, String8& value); + + size_t size() { return mParameters.size(); } + +private: + String8 mKeyValuePairs; + KeyedVector <String8, String8> mParameters; +}; + +}; // namespace android + +#pragma GCC visibility pop + +#endif /*ANDROID_AUDIOSYSTEM_H_*/ diff --git a/dom/system/gonk/android_audio/AudioTrack.h b/dom/system/gonk/android_audio/AudioTrack.h new file mode 100644 index 000000000..6f8c6bb28 --- /dev/null +++ b/dom/system/gonk/android_audio/AudioTrack.h @@ -0,0 +1,489 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_AUDIOTRACK_H +#define ANDROID_AUDIOTRACK_H + +#include <stdint.h> +#include <sys/types.h> + +#include "IAudioFlinger.h" +#include "IAudioTrack.h" +#include "AudioSystem.h" + +#include <utils/RefBase.h> +#include <utils/Errors.h> +#include <binder/IInterface.h> +#include <binder/IMemory.h> +#include <utils/threads.h> + + +namespace android { + +// ---------------------------------------------------------------------------- + +class audio_track_cblk_t; + +// ---------------------------------------------------------------------------- + +class AudioTrack +{ +public: + enum channel_index { + MONO = 0, + LEFT = 0, + RIGHT = 1 + }; + + /* Events used by AudioTrack callback function (audio_track_cblk_t). + */ + enum event_type { + EVENT_MORE_DATA = 0, // Request to write more data to PCM buffer. + EVENT_UNDERRUN = 1, // PCM buffer underrun occured. + EVENT_LOOP_END = 2, // Sample loop end was reached; playback restarted from loop start if loop count was not 0. + EVENT_MARKER = 3, // Playback head is at the specified marker position (See setMarkerPosition()). + EVENT_NEW_POS = 4, // Playback head is at a new position (See setPositionUpdatePeriod()). + EVENT_BUFFER_END = 5 // Playback head is at the end of the buffer. + }; + + /* Create Buffer on the stack and pass it to obtainBuffer() + * and releaseBuffer(). + */ + + class Buffer + { + public: + enum { + MUTE = 0x00000001 + }; + uint32_t flags; + int channelCount; + int format; + size_t frameCount; + size_t size; + union { + void* raw; + short* i16; + int8_t* i8; + }; + }; + + + /* As a convenience, if a callback is supplied, a handler thread + * is automatically created with the appropriate priority. This thread + * invokes the callback when a new buffer becomes availlable or an underrun condition occurs. + * Parameters: + * + * event: type of event notified (see enum AudioTrack::event_type). + * user: Pointer to context for use by the callback receiver. + * info: Pointer to optional parameter according to event type: + * - EVENT_MORE_DATA: pointer to AudioTrack::Buffer struct. The callback must not write + * more bytes than indicated by 'size' field and update 'size' if less bytes are + * written. + * - EVENT_UNDERRUN: unused. + * - EVENT_LOOP_END: pointer to an int indicating the number of loops remaining. + * - EVENT_MARKER: pointer to an uin32_t containing the marker position in frames. + * - EVENT_NEW_POS: pointer to an uin32_t containing the new position in frames. + * - EVENT_BUFFER_END: unused. + */ + + typedef void (*callback_t)(int event, void* user, void *info); + + /* Returns the minimum frame count required for the successful creation of + * an AudioTrack object. + * Returned status (from utils/Errors.h) can be: + * - NO_ERROR: successful operation + * - NO_INIT: audio server or audio hardware not initialized + */ + + static status_t getMinFrameCount(int* frameCount, + int streamType =-1, + uint32_t sampleRate = 0); + + /* Constructs an uninitialized AudioTrack. No connection with + * AudioFlinger takes place. + */ + AudioTrack(); + + /* Creates an audio track and registers it with AudioFlinger. + * Once created, the track needs to be started before it can be used. + * Unspecified values are set to the audio hardware's current + * values. + * + * Parameters: + * + * streamType: Select the type of audio stream this track is attached to + * (e.g. AudioSystem::MUSIC). + * sampleRate: Track sampling rate in Hz. + * format: Audio format (e.g AudioSystem::PCM_16_BIT for signed + * 16 bits per sample). + * channels: Channel mask: see AudioSystem::audio_channels. + * frameCount: Total size of track PCM buffer in frames. This defines the + * latency of the track. + * flags: Reserved for future use. + * cbf: Callback function. If not null, this function is called periodically + * to request new PCM data. + * notificationFrames: The callback function is called each time notificationFrames PCM + * frames have been comsumed from track input buffer. + * user Context for use by the callback receiver. + */ + + AudioTrack( int streamType, + uint32_t sampleRate = 0, + int format = 0, + int channels = 0, + int frameCount = 0, + uint32_t flags = 0, + callback_t cbf = 0, + void* user = 0, + int notificationFrames = 0, + int sessionId = 0); + + /* Creates an audio track and registers it with AudioFlinger. With this constructor, + * The PCM data to be rendered by AudioTrack is passed in a shared memory buffer + * identified by the argument sharedBuffer. This prototype is for static buffer playback. + * PCM data must be present into memory before the AudioTrack is started. + * The Write() and Flush() methods are not supported in this case. + * It is recommented to pass a callback function to be notified of playback end by an + * EVENT_UNDERRUN event. + */ + + AudioTrack( int streamType, + uint32_t sampleRate = 0, + int format = 0, + int channels = 0, + const sp<IMemory>& sharedBuffer = 0, + uint32_t flags = 0, + callback_t cbf = 0, + void* user = 0, + int notificationFrames = 0, + int sessionId = 0); + + /* Terminates the AudioTrack and unregisters it from AudioFlinger. + * Also destroys all resources assotiated with the AudioTrack. + */ + ~AudioTrack(); + + + /* Initialize an uninitialized AudioTrack. + * Returned status (from utils/Errors.h) can be: + * - NO_ERROR: successful intialization + * - INVALID_OPERATION: AudioTrack is already intitialized + * - BAD_VALUE: invalid parameter (channels, format, sampleRate...) + * - NO_INIT: audio server or audio hardware not initialized + * */ + status_t set(int streamType =-1, + uint32_t sampleRate = 0, + int format = 0, + int channels = 0, + int frameCount = 0, + uint32_t flags = 0, + callback_t cbf = 0, + void* user = 0, + int notificationFrames = 0, + const sp<IMemory>& sharedBuffer = 0, + bool threadCanCallJava = false, + int sessionId = 0); + + + /* Result of constructing the AudioTrack. This must be checked + * before using any AudioTrack API (except for set()), using + * an uninitialized AudioTrack produces undefined results. + * See set() method above for possible return codes. + */ + status_t initCheck() const; + + /* Returns this track's latency in milliseconds. + * This includes the latency due to AudioTrack buffer size, AudioMixer (if any) + * and audio hardware driver. + */ + uint32_t latency() const; + + /* getters, see constructor */ + + int streamType() const; + int format() const; + int channelCount() const; + uint32_t frameCount() const; + int frameSize() const; + sp<IMemory>& sharedBuffer(); + + + /* After it's created the track is not active. Call start() to + * make it active. If set, the callback will start being called. + */ + void start(); + + /* Stop a track. If set, the callback will cease being called and + * obtainBuffer returns STOPPED. Note that obtainBuffer() still works + * and will fill up buffers until the pool is exhausted. + */ + void stop(); + bool stopped() const; + + /* flush a stopped track. All pending buffers are discarded. + * This function has no effect if the track is not stoped. + */ + void flush(); + + /* Pause a track. If set, the callback will cease being called and + * obtainBuffer returns STOPPED. Note that obtainBuffer() still works + * and will fill up buffers until the pool is exhausted. + */ + void pause(); + + /* mute or unmutes this track. + * While mutted, the callback, if set, is still called. + */ + void mute(bool); + bool muted() const; + + + /* set volume for this track, mostly used for games' sound effects + * left and right volumes. Levels must be <= 1.0. + */ + status_t setVolume(float left, float right); + void getVolume(float* left, float* right); + + /* set the send level for this track. An auxiliary effect should be attached + * to the track with attachEffect(). Level must be <= 1.0. + */ + status_t setAuxEffectSendLevel(float level); + void getAuxEffectSendLevel(float* level); + + /* set sample rate for this track, mostly used for games' sound effects + */ + status_t setSampleRate(int sampleRate); + uint32_t getSampleRate(); + + /* Enables looping and sets the start and end points of looping. + * + * Parameters: + * + * loopStart: loop start expressed as the number of PCM frames played since AudioTrack start. + * loopEnd: loop end expressed as the number of PCM frames played since AudioTrack start. + * loopCount: number of loops to execute. Calling setLoop() with loopCount == 0 cancels any pending or + * active loop. loopCount = -1 means infinite looping. + * + * For proper operation the following condition must be respected: + * (loopEnd-loopStart) <= framecount() + */ + status_t setLoop(uint32_t loopStart, uint32_t loopEnd, int loopCount); + status_t getLoop(uint32_t *loopStart, uint32_t *loopEnd, int *loopCount); + + + /* Sets marker position. When playback reaches the number of frames specified, a callback with event + * type EVENT_MARKER is called. Calling setMarkerPosition with marker == 0 cancels marker notification + * callback. + * If the AudioTrack has been opened with no callback function associated, the operation will fail. + * + * Parameters: + * + * marker: marker position expressed in frames. + * + * Returned status (from utils/Errors.h) can be: + * - NO_ERROR: successful operation + * - INVALID_OPERATION: the AudioTrack has no callback installed. + */ + status_t setMarkerPosition(uint32_t marker); + status_t getMarkerPosition(uint32_t *marker); + + + /* Sets position update period. Every time the number of frames specified has been played, + * a callback with event type EVENT_NEW_POS is called. + * Calling setPositionUpdatePeriod with updatePeriod == 0 cancels new position notification + * callback. + * If the AudioTrack has been opened with no callback function associated, the operation will fail. + * + * Parameters: + * + * updatePeriod: position update notification period expressed in frames. + * + * Returned status (from utils/Errors.h) can be: + * - NO_ERROR: successful operation + * - INVALID_OPERATION: the AudioTrack has no callback installed. + */ + status_t setPositionUpdatePeriod(uint32_t updatePeriod); + status_t getPositionUpdatePeriod(uint32_t *updatePeriod); + + + /* Sets playback head position within AudioTrack buffer. The new position is specified + * in number of frames. + * This method must be called with the AudioTrack in paused or stopped state. + * Note that the actual position set is <position> modulo the AudioTrack buffer size in frames. + * Therefore using this method makes sense only when playing a "static" audio buffer + * as opposed to streaming. + * The getPosition() method on the other hand returns the total number of frames played since + * playback start. + * + * Parameters: + * + * position: New playback head position within AudioTrack buffer. + * + * Returned status (from utils/Errors.h) can be: + * - NO_ERROR: successful operation + * - INVALID_OPERATION: the AudioTrack is not stopped. + * - BAD_VALUE: The specified position is beyond the number of frames present in AudioTrack buffer + */ + status_t setPosition(uint32_t position); + status_t getPosition(uint32_t *position); + + /* Forces AudioTrack buffer full condition. When playing a static buffer, this method avoids + * rewriting the buffer before restarting playback after a stop. + * This method must be called with the AudioTrack in paused or stopped state. + * + * Returned status (from utils/Errors.h) can be: + * - NO_ERROR: successful operation + * - INVALID_OPERATION: the AudioTrack is not stopped. + */ + status_t reload(); + + /* returns a handle on the audio output used by this AudioTrack. + * + * Parameters: + * none. + * + * Returned value: + * handle on audio hardware output + */ + audio_io_handle_t getOutput(); + + /* returns the unique ID associated to this track. + * + * Parameters: + * none. + * + * Returned value: + * AudioTrack ID. + */ + int getSessionId(); + + + /* Attach track auxiliary output to specified effect. Used effectId = 0 + * to detach track from effect. + * + * Parameters: + * + * effectId: effectId obtained from AudioEffect::id(). + * + * Returned status (from utils/Errors.h) can be: + * - NO_ERROR: successful operation + * - INVALID_OPERATION: the effect is not an auxiliary effect. + * - BAD_VALUE: The specified effect ID is invalid + */ + status_t attachAuxEffect(int effectId); + + /* obtains a buffer of "frameCount" frames. The buffer must be + * filled entirely. If the track is stopped, obtainBuffer() returns + * STOPPED instead of NO_ERROR as long as there are buffers availlable, + * at which point NO_MORE_BUFFERS is returned. + * Buffers will be returned until the pool (buffercount()) + * is exhausted, at which point obtainBuffer() will either block + * or return WOULD_BLOCK depending on the value of the "blocking" + * parameter. + */ + + enum { + NO_MORE_BUFFERS = 0x80000001, + STOPPED = 1 + }; + + status_t obtainBuffer(Buffer* audioBuffer, int32_t waitCount); + void releaseBuffer(Buffer* audioBuffer); + + + /* As a convenience we provide a write() interface to the audio buffer. + * This is implemented on top of lockBuffer/unlockBuffer. For best + * performance + * + */ + ssize_t write(const void* buffer, size_t size); + + /* + * Dumps the state of an audio track. + */ + status_t dump(int fd, const Vector<String16>& args) const; + +private: + /* copying audio tracks is not allowed */ + AudioTrack(const AudioTrack& other); + AudioTrack& operator = (const AudioTrack& other); + + /* a small internal class to handle the callback */ + class AudioTrackThread : public Thread + { + public: + AudioTrackThread(AudioTrack& receiver, bool bCanCallJava = false); + private: + friend class AudioTrack; + virtual bool threadLoop(); + virtual status_t readyToRun(); + virtual void onFirstRef(); + AudioTrack& mReceiver; + Mutex mLock; + }; + + bool processAudioBuffer(const sp<AudioTrackThread>& thread); + status_t createTrack(int streamType, + uint32_t sampleRate, + int format, + int channelCount, + int frameCount, + uint32_t flags, + const sp<IMemory>& sharedBuffer, + audio_io_handle_t output, + bool enforceFrameCount); + + sp<IAudioTrack> mAudioTrack; + sp<IMemory> mCblkMemory; + sp<AudioTrackThread> mAudioTrackThread; + + float mVolume[2]; + float mSendLevel; + uint32_t mFrameCount; + + audio_track_cblk_t* mCblk; + uint8_t mStreamType; + uint8_t mFormat; + uint8_t mChannelCount; + uint8_t mMuted; + uint32_t mChannels; + status_t mStatus; + uint32_t mLatency; + + volatile int32_t mActive; + + callback_t mCbf; + void* mUserData; + uint32_t mNotificationFramesReq; // requested number of frames between each notification callback + uint32_t mNotificationFramesAct; // actual number of frames between each notification callback + sp<IMemory> mSharedBuffer; + int mLoopCount; + uint32_t mRemainingFrames; + uint32_t mMarkerPosition; + bool mMarkerReached; + uint32_t mNewPosition; + uint32_t mUpdatePeriod; + uint32_t mFlags; + int mSessionId; + int mAuxEffectId; + uint32_t mPadding[8]; +}; + + +}; // namespace android + +#endif // ANDROID_AUDIOTRACK_H diff --git a/dom/system/gonk/android_audio/EffectApi.h b/dom/system/gonk/android_audio/EffectApi.h new file mode 100644 index 000000000..729545d0c --- /dev/null +++ b/dom/system/gonk/android_audio/EffectApi.h @@ -0,0 +1,798 @@ +/* + * Copyright (C) 2010 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. + */ + +#ifndef ANDROID_EFFECTAPI_H_ +#define ANDROID_EFFECTAPI_H_ + +#include <errno.h> +#include <stdint.h> +#include <sys/types.h> + +#if __cplusplus +extern "C" { +#endif + +///////////////////////////////////////////////// +// Effect control interface +///////////////////////////////////////////////// + +// The effect control interface is exposed by each effect engine implementation. It consists of +// a set of functions controlling the configuration, activation and process of the engine. +// The functions are grouped in a structure of type effect_interface_s: +// struct effect_interface_s { +// effect_process_t process; +// effect_command_t command; +// }; + + +// effect_interface_t: Effect control interface handle. +// The effect_interface_t serves two purposes regarding the implementation of the effect engine: +// - 1 it is the address of a pointer to an effect_interface_s structure where the functions +// of the effect control API for a particular effect are located. +// - 2 it is the address of the context of a particular effect instance. +// A typical implementation in the effect library would define a structure as follows: +// struct effect_module_s { +// const struct effect_interface_s *itfe; +// effect_config_t config; +// effect_context_t context; +// } +// The implementation of EffectCreate() function would then allocate a structure of this +// type and return its address as effect_interface_t +typedef struct effect_interface_s **effect_interface_t; + + +// Effect API version 1.0 +#define EFFECT_API_VERSION 0x0100 // Format 0xMMmm MM: Major version, mm: minor version + +// Maximum length of character strings in structures defines by this API. +#define EFFECT_STRING_LEN_MAX 64 + +// +//--- Effect descriptor structure effect_descriptor_t +// + +// Unique effect ID (can be generated from the following site: +// http://www.itu.int/ITU-T/asn1/uuid.html) +// This format is used for both "type" and "uuid" fields of the effect descriptor structure. +// - When used for effect type and the engine is implementing and effect corresponding to a standard +// OpenSL ES interface, this ID must be the one defined in OpenSLES_IID.h for that interface. +// - When used as uuid, it should be a unique UUID for this particular implementation. +typedef struct effect_uuid_s { + uint32_t timeLow; + uint16_t timeMid; + uint16_t timeHiAndVersion; + uint16_t clockSeq; + uint8_t node[6]; +} effect_uuid_t; + +// NULL UUID definition (matches SL_IID_NULL_) +#define EFFECT_UUID_INITIALIZER { 0xec7178ec, 0xe5e1, 0x4432, 0xa3f4, \ + { 0x46, 0x57, 0xe6, 0x79, 0x52, 0x10 } } +static const effect_uuid_t EFFECT_UUID_NULL_ = EFFECT_UUID_INITIALIZER; +const effect_uuid_t * const EFFECT_UUID_NULL = &EFFECT_UUID_NULL_; +const char * const EFFECT_UUID_NULL_STR = "ec7178ec-e5e1-4432-a3f4-4657e6795210"; + +// The effect descriptor contains necessary information to facilitate the enumeration of the effect +// engines present in a library. +typedef struct effect_descriptor_s { + effect_uuid_t type; // UUID of to the OpenSL ES interface implemented by this effect + effect_uuid_t uuid; // UUID for this particular implementation + uint16_t apiVersion; // Version of the effect API implemented: matches EFFECT_API_VERSION + uint32_t flags; // effect engine capabilities/requirements flags (see below) + uint16_t cpuLoad; // CPU load indication (see below) + uint16_t memoryUsage; // Data Memory usage (see below) + char name[EFFECT_STRING_LEN_MAX]; // human readable effect name + char implementor[EFFECT_STRING_LEN_MAX]; // human readable effect implementor name +} effect_descriptor_t; + +// CPU load and memory usage indication: each effect implementation must provide an indication of +// its CPU and memory usage for the audio effect framework to limit the number of effects +// instantiated at a given time on a given platform. +// The CPU load is expressed in 0.1 MIPS units as estimated on an ARM9E core (ARMv5TE) with 0 WS. +// The memory usage is expressed in KB and includes only dynamically allocated memory + +// Definitions for flags field of effect descriptor. +// +---------------------------+-----------+----------------------------------- +// | description | bits | values +// +---------------------------+-----------+----------------------------------- +// | connection mode | 0..1 | 0 insert: after track process +// | | | 1 auxiliary: connect to track auxiliary +// | | | output and use send level +// | | | 2 replace: replaces track process function; +// | | | must implement SRC, volume and mono to stereo. +// | | | 3 reserved +// +---------------------------+-----------+----------------------------------- +// | insertion preference | 2..4 | 0 none +// | | | 1 first of the chain +// | | | 2 last of the chain +// | | | 3 exclusive (only effect in the insert chain) +// | | | 4..7 reserved +// +---------------------------+-----------+----------------------------------- +// | Volume management | 5..6 | 0 none +// | | | 1 implements volume control +// | | | 2 requires volume indication +// | | | 3 reserved +// +---------------------------+-----------+----------------------------------- +// | Device indication | 7..8 | 0 none +// | | | 1 requires device updates +// | | | 2..3 reserved +// +---------------------------+-----------+----------------------------------- +// | Sample input mode | 9..10 | 0 direct: process() function or EFFECT_CMD_CONFIGURE +// | | | command must specify a buffer descriptor +// | | | 1 provider: process() function uses the +// | | | bufferProvider indicated by the +// | | | EFFECT_CMD_CONFIGURE command to request input. +// | | | buffers. +// | | | 2 both: both input modes are supported +// | | | 3 reserved +// +---------------------------+-----------+----------------------------------- +// | Sample output mode | 11..12 | 0 direct: process() function or EFFECT_CMD_CONFIGURE +// | | | command must specify a buffer descriptor +// | | | 1 provider: process() function uses the +// | | | bufferProvider indicated by the +// | | | EFFECT_CMD_CONFIGURE command to request output +// | | | buffers. +// | | | 2 both: both output modes are supported +// | | | 3 reserved +// +---------------------------+-----------+----------------------------------- +// | Hardware acceleration | 13..15 | 0 No hardware acceleration +// | | | 1 non tunneled hw acceleration: the process() function +// | | | reads the samples, send them to HW accelerated +// | | | effect processor, reads back the processed samples +// | | | and returns them to the output buffer. +// | | | 2 tunneled hw acceleration: the process() function is +// | | | transparent. The effect interface is only used to +// | | | control the effect engine. This mode is relevant for +// | | | global effects actually applied by the audio +// | | | hardware on the output stream. +// +---------------------------+-----------+----------------------------------- +// | Audio Mode indication | 16..17 | 0 none +// | | | 1 requires audio mode updates +// | | | 2..3 reserved +// +---------------------------+-----------+----------------------------------- + +// Insert mode +#define EFFECT_FLAG_TYPE_MASK 0x00000003 +#define EFFECT_FLAG_TYPE_INSERT 0x00000000 +#define EFFECT_FLAG_TYPE_AUXILIARY 0x00000001 +#define EFFECT_FLAG_TYPE_REPLACE 0x00000002 + +// Insert preference +#define EFFECT_FLAG_INSERT_MASK 0x0000001C +#define EFFECT_FLAG_INSERT_ANY 0x00000000 +#define EFFECT_FLAG_INSERT_FIRST 0x00000004 +#define EFFECT_FLAG_INSERT_LAST 0x00000008 +#define EFFECT_FLAG_INSERT_EXCLUSIVE 0x0000000C + + +// Volume control +#define EFFECT_FLAG_VOLUME_MASK 0x00000060 +#define EFFECT_FLAG_VOLUME_CTRL 0x00000020 +#define EFFECT_FLAG_VOLUME_IND 0x00000040 +#define EFFECT_FLAG_VOLUME_NONE 0x00000000 + +// Device indication +#define EFFECT_FLAG_DEVICE_MASK 0x00000180 +#define EFFECT_FLAG_DEVICE_IND 0x00000080 +#define EFFECT_FLAG_DEVICE_NONE 0x00000000 + +// Sample input modes +#define EFFECT_FLAG_INPUT_MASK 0x00000600 +#define EFFECT_FLAG_INPUT_DIRECT 0x00000000 +#define EFFECT_FLAG_INPUT_PROVIDER 0x00000200 +#define EFFECT_FLAG_INPUT_BOTH 0x00000400 + +// Sample output modes +#define EFFECT_FLAG_OUTPUT_MASK 0x00001800 +#define EFFECT_FLAG_OUTPUT_DIRECT 0x00000000 +#define EFFECT_FLAG_OUTPUT_PROVIDER 0x00000800 +#define EFFECT_FLAG_OUTPUT_BOTH 0x00001000 + +// Hardware acceleration mode +#define EFFECT_FLAG_HW_ACC_MASK 0x00006000 +#define EFFECT_FLAG_HW_ACC_SIMPLE 0x00002000 +#define EFFECT_FLAG_HW_ACC_TUNNEL 0x00004000 + +// Audio mode indication +#define EFFECT_FLAG_AUDIO_MODE_MASK 0x00018000 +#define EFFECT_FLAG_AUDIO_MODE_IND 0x00008000 +#define EFFECT_FLAG_AUDIO_MODE_NONE 0x00000000 + +// Forward definition of type audio_buffer_t +typedef struct audio_buffer_s audio_buffer_t; + +//////////////////////////////////////////////////////////////////////////////// +// +// Function: process +// +// Description: Effect process function. Takes input samples as specified +// (count and location) in input buffer descriptor and output processed +// samples as specified in output buffer descriptor. If the buffer descriptor +// is not specified the function must use either the buffer or the +// buffer provider function installed by the EFFECT_CMD_CONFIGURE command. +// The effect framework will call the process() function after the EFFECT_CMD_ENABLE +// command is received and until the EFFECT_CMD_DISABLE is received. When the engine +// receives the EFFECT_CMD_DISABLE command it should turn off the effect gracefully +// and when done indicate that it is OK to stop calling the process() function by +// returning the -ENODATA status. +// +// NOTE: the process() function implementation should be "real-time safe" that is +// it should not perform blocking calls: malloc/free, sleep, read/write/open/close, +// pthread_cond_wait/pthread_mutex_lock... +// +// Input: +// effect_interface_t: handle to the effect interface this function +// is called on. +// inBuffer: buffer descriptor indicating where to read samples to process. +// If NULL, use the configuration passed by EFFECT_CMD_CONFIGURE command. +// +// inBuffer: buffer descriptor indicating where to write processed samples. +// If NULL, use the configuration passed by EFFECT_CMD_CONFIGURE command. +// +// Output: +// returned value: 0 successful operation +// -ENODATA the engine has finished the disable phase and the framework +// can stop calling process() +// -EINVAL invalid interface handle or +// invalid input/output buffer description +//////////////////////////////////////////////////////////////////////////////// +typedef int32_t (*effect_process_t)(effect_interface_t self, + audio_buffer_t *inBuffer, + audio_buffer_t *outBuffer); + +//////////////////////////////////////////////////////////////////////////////// +// +// Function: command +// +// Description: Send a command and receive a response to/from effect engine. +// +// Input: +// effect_interface_t: handle to the effect interface this function +// is called on. +// cmdCode: command code: the command can be a standardized command defined in +// effect_command_e (see below) or a proprietary command. +// cmdSize: size of command in bytes +// pCmdData: pointer to command data +// pReplyData: pointer to reply data +// +// Input/Output: +// replySize: maximum size of reply data as input +// actual size of reply data as output +// +// Output: +// returned value: 0 successful operation +// -EINVAL invalid interface handle or +// invalid command/reply size or format according to command code +// The return code should be restricted to indicate problems related to the this +// API specification. Status related to the execution of a particular command should be +// indicated as part of the reply field. +// +// *pReplyData updated with command response +// +//////////////////////////////////////////////////////////////////////////////// +typedef int32_t (*effect_command_t)(effect_interface_t self, + uint32_t cmdCode, + uint32_t cmdSize, + void *pCmdData, + uint32_t *replySize, + void *pReplyData); + + +// Effect control interface definition +struct effect_interface_s { + effect_process_t process; + effect_command_t command; +}; + + +// +//--- Standardized command codes for command() function +// +enum effect_command_e { + EFFECT_CMD_INIT, // initialize effect engine + EFFECT_CMD_CONFIGURE, // configure effect engine (see effect_config_t) + EFFECT_CMD_RESET, // reset effect engine + EFFECT_CMD_ENABLE, // enable effect process + EFFECT_CMD_DISABLE, // disable effect process + EFFECT_CMD_SET_PARAM, // set parameter immediately (see effect_param_t) + EFFECT_CMD_SET_PARAM_DEFERRED, // set parameter deferred + EFFECT_CMD_SET_PARAM_COMMIT, // commit previous set parameter deferred + EFFECT_CMD_GET_PARAM, // get parameter + EFFECT_CMD_SET_DEVICE, // set audio device (see audio_device_e) + EFFECT_CMD_SET_VOLUME, // set volume + EFFECT_CMD_SET_AUDIO_MODE, // set the audio mode (normal, ring, ...) + EFFECT_CMD_FIRST_PROPRIETARY = 0x10000 // first proprietary command code +}; + +//================================================================================================== +// command: EFFECT_CMD_INIT +//-------------------------------------------------------------------------------------------------- +// description: +// Initialize effect engine: All configurations return to default +//-------------------------------------------------------------------------------------------------- +// command format: +// size: 0 +// data: N/A +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: sizeof(int) +// data: status +//================================================================================================== +// command: EFFECT_CMD_CONFIGURE +//-------------------------------------------------------------------------------------------------- +// description: +// Apply new audio parameters configurations for input and output buffers +//-------------------------------------------------------------------------------------------------- +// command format: +// size: sizeof(effect_config_t) +// data: effect_config_t +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: sizeof(int) +// data: status +//================================================================================================== +// command: EFFECT_CMD_RESET +//-------------------------------------------------------------------------------------------------- +// description: +// Reset the effect engine. Keep configuration but resets state and buffer content +//-------------------------------------------------------------------------------------------------- +// command format: +// size: 0 +// data: N/A +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: 0 +// data: N/A +//================================================================================================== +// command: EFFECT_CMD_ENABLE +//-------------------------------------------------------------------------------------------------- +// description: +// Enable the process. Called by the framework before the first call to process() +//-------------------------------------------------------------------------------------------------- +// command format: +// size: 0 +// data: N/A +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: sizeof(int) +// data: status +//================================================================================================== +// command: EFFECT_CMD_DISABLE +//-------------------------------------------------------------------------------------------------- +// description: +// Disable the process. Called by the framework after the last call to process() +//-------------------------------------------------------------------------------------------------- +// command format: +// size: 0 +// data: N/A +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: sizeof(int) +// data: status +//================================================================================================== +// command: EFFECT_CMD_SET_PARAM +//-------------------------------------------------------------------------------------------------- +// description: +// Set a parameter and apply it immediately +//-------------------------------------------------------------------------------------------------- +// command format: +// size: sizeof(effect_param_t) + size of param and value +// data: effect_param_t + param + value. See effect_param_t definition below for value offset +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: sizeof(int) +// data: status +//================================================================================================== +// command: EFFECT_CMD_SET_PARAM_DEFERRED +//-------------------------------------------------------------------------------------------------- +// description: +// Set a parameter but apply it only when receiving EFFECT_CMD_SET_PARAM_COMMIT command +//-------------------------------------------------------------------------------------------------- +// command format: +// size: sizeof(effect_param_t) + size of param and value +// data: effect_param_t + param + value. See effect_param_t definition below for value offset +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: 0 +// data: N/A +//================================================================================================== +// command: EFFECT_CMD_SET_PARAM_COMMIT +//-------------------------------------------------------------------------------------------------- +// description: +// Apply all previously received EFFECT_CMD_SET_PARAM_DEFERRED commands +//-------------------------------------------------------------------------------------------------- +// command format: +// size: 0 +// data: N/A +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: sizeof(int) +// data: status +//================================================================================================== +// command: EFFECT_CMD_GET_PARAM +//-------------------------------------------------------------------------------------------------- +// description: +// Get a parameter value +//-------------------------------------------------------------------------------------------------- +// command format: +// size: sizeof(effect_param_t) + size of param +// data: effect_param_t + param +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: sizeof(effect_param_t) + size of param and value +// data: effect_param_t + param + value. See effect_param_t definition below for value offset +//================================================================================================== +// command: EFFECT_CMD_SET_DEVICE +//-------------------------------------------------------------------------------------------------- +// description: +// Set the rendering device the audio output path is connected to. See audio_device_e for device +// values. +// The effect implementation must set EFFECT_FLAG_DEVICE_IND flag in its descriptor to receive this +// command when the device changes +//-------------------------------------------------------------------------------------------------- +// command format: +// size: sizeof(uint32_t) +// data: audio_device_e +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: 0 +// data: N/A +//================================================================================================== +// command: EFFECT_CMD_SET_VOLUME +//-------------------------------------------------------------------------------------------------- +// description: +// Set and get volume. Used by audio framework to delegate volume control to effect engine. +// The effect implementation must set EFFECT_FLAG_VOLUME_IND or EFFECT_FLAG_VOLUME_CTRL flag in +// its descriptor to receive this command before every call to process() function +// If EFFECT_FLAG_VOLUME_CTRL flag is set in the effect descriptor, the effect engine must return +// the volume that should be applied before the effect is processed. The overall volume (the volume +// actually applied by the effect engine multiplied by the returned value) should match the value +// indicated in the command. +//-------------------------------------------------------------------------------------------------- +// command format: +// size: n * sizeof(uint32_t) +// data: volume for each channel defined in effect_config_t for output buffer expressed in +// 8.24 fixed point format +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: n * sizeof(uint32_t) / 0 +// data: - if EFFECT_FLAG_VOLUME_CTRL is set in effect descriptor: +// volume for each channel defined in effect_config_t for output buffer expressed in +// 8.24 fixed point format +// - if EFFECT_FLAG_VOLUME_CTRL is not set in effect descriptor: +// N/A +// It is legal to receive a null pointer as pReplyData in which case the effect framework has +// delegated volume control to another effect +//================================================================================================== +// command: EFFECT_CMD_SET_AUDIO_MODE +//-------------------------------------------------------------------------------------------------- +// description: +// Set the audio mode. The effect implementation must set EFFECT_FLAG_AUDIO_MODE_IND flag in its +// descriptor to receive this command when the audio mode changes. +//-------------------------------------------------------------------------------------------------- +// command format: +// size: sizeof(uint32_t) +// data: audio_mode_e +//-------------------------------------------------------------------------------------------------- +// reply format: +// size: 0 +// data: N/A +//================================================================================================== +// command: EFFECT_CMD_FIRST_PROPRIETARY +//-------------------------------------------------------------------------------------------------- +// description: +// All proprietary effect commands must use command codes above this value. The size and format of +// command and response fields is free in this case +//================================================================================================== + + +// Audio buffer descriptor used by process(), bufferProvider() functions and buffer_config_t +// structure. Multi-channel audio is always interleaved. The channel order is from LSB to MSB with +// regard to the channel mask definition in audio_channels_e e.g : +// Stereo: left, right +// 5 point 1: front left, front right, front center, low frequency, back left, back right +// The buffer size is expressed in frame count, a frame being composed of samples for all +// channels at a given time. Frame size for unspecified format (AUDIO_FORMAT_OTHER) is 8 bit by +// definition +struct audio_buffer_s { + size_t frameCount; // number of frames in buffer + union { + void* raw; // raw pointer to start of buffer + int32_t* s32; // pointer to signed 32 bit data at start of buffer + int16_t* s16; // pointer to signed 16 bit data at start of buffer + uint8_t* u8; // pointer to unsigned 8 bit data at start of buffer + }; +}; + +// The buffer_provider_s structure contains functions that can be used +// by the effect engine process() function to query and release input +// or output audio buffer. +// The getBuffer() function is called to retrieve a buffer where data +// should read from or written to by process() function. +// The releaseBuffer() function MUST be called when the buffer retrieved +// with getBuffer() is not needed anymore. +// The process function should use the buffer provider mechanism to retrieve +// input or output buffer if the inBuffer or outBuffer passed as argument is NULL +// and the buffer configuration (buffer_config_t) given by the EFFECT_CMD_CONFIGURE +// command did not specify an audio buffer. + +typedef int32_t (* buffer_function_t)(void *cookie, audio_buffer_t *buffer); + +typedef struct buffer_provider_s { + buffer_function_t getBuffer; // retrieve next buffer + buffer_function_t releaseBuffer; // release used buffer + void *cookie; // for use by client of buffer provider functions +} buffer_provider_t; + + +// The buffer_config_s structure specifies the input or output audio format +// to be used by the effect engine. It is part of the effect_config_t +// structure that defines both input and output buffer configurations and is +// passed by the EFFECT_CMD_CONFIGURE command. +typedef struct buffer_config_s { + audio_buffer_t buffer; // buffer for use by process() function if not passed explicitly + uint32_t samplingRate; // sampling rate + uint32_t channels; // channel mask (see audio_channels_e) + buffer_provider_t bufferProvider; // buffer provider + uint8_t format; // Audio format (see audio_format_e) + uint8_t accessMode; // read/write or accumulate in buffer (effect_buffer_access_e) + uint16_t mask; // indicates which of the above fields is valid +} buffer_config_t; + +// Sample format +enum audio_format_e { + SAMPLE_FORMAT_PCM_S15, // PCM signed 16 bits + SAMPLE_FORMAT_PCM_U8, // PCM unsigned 8 bits + SAMPLE_FORMAT_PCM_S7_24, // PCM signed 7.24 fixed point representation + SAMPLE_FORMAT_OTHER // other format (e.g. compressed) +}; + +// Channel mask +enum audio_channels_e { + CHANNEL_FRONT_LEFT = 0x1, // front left channel + CHANNEL_FRONT_RIGHT = 0x2, // front right channel + CHANNEL_FRONT_CENTER = 0x4, // front center channel + CHANNEL_LOW_FREQUENCY = 0x8, // low frequency channel + CHANNEL_BACK_LEFT = 0x10, // back left channel + CHANNEL_BACK_RIGHT = 0x20, // back right channel + CHANNEL_FRONT_LEFT_OF_CENTER = 0x40, // front left of center channel + CHANNEL_FRONT_RIGHT_OF_CENTER = 0x80, // front right of center channel + CHANNEL_BACK_CENTER = 0x100, // back center channel + CHANNEL_MONO = CHANNEL_FRONT_LEFT, + CHANNEL_STEREO = (CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT), + CHANNEL_QUAD = (CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_BACK_LEFT | CHANNEL_BACK_RIGHT), + CHANNEL_SURROUND = (CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | CHANNEL_BACK_CENTER), + CHANNEL_5POINT1 = (CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | CHANNEL_LOW_FREQUENCY | CHANNEL_BACK_LEFT | CHANNEL_BACK_RIGHT), + CHANNEL_7POINT1 = (CHANNEL_FRONT_LEFT | CHANNEL_FRONT_RIGHT | + CHANNEL_FRONT_CENTER | CHANNEL_LOW_FREQUENCY | CHANNEL_BACK_LEFT | CHANNEL_BACK_RIGHT | + CHANNEL_FRONT_LEFT_OF_CENTER | CHANNEL_FRONT_RIGHT_OF_CENTER), +}; + +// Render device +enum audio_device_e { + DEVICE_EARPIECE = 0x1, // earpiece + DEVICE_SPEAKER = 0x2, // speaker + DEVICE_WIRED_HEADSET = 0x4, // wired headset, with microphone + DEVICE_WIRED_HEADPHONE = 0x8, // wired headphone, without microphone + DEVICE_BLUETOOTH_SCO = 0x10, // generic bluetooth SCO + DEVICE_BLUETOOTH_SCO_HEADSET = 0x20, // bluetooth SCO headset + DEVICE_BLUETOOTH_SCO_CARKIT = 0x40, // bluetooth SCO car kit + DEVICE_BLUETOOTH_A2DP = 0x80, // generic bluetooth A2DP + DEVICE_BLUETOOTH_A2DP_HEADPHONES = 0x100, // bluetooth A2DP headphones + DEVICE_BLUETOOTH_A2DP_SPEAKER = 0x200, // bluetooth A2DP speakers + DEVICE_AUX_DIGITAL = 0x400, // digital output + DEVICE_EXTERNAL_SPEAKER = 0x800 // external speaker (stereo and High quality) +}; + +#if ANDROID_VERSION < 17 +// Audio mode +enum audio_mode_e { + AUDIO_MODE_NORMAL, // device idle + AUDIO_MODE_RINGTONE, // device ringing + AUDIO_MODE_IN_CALL // audio call connected (VoIP or telephony) +}; +#endif + +// Values for "accessMode" field of buffer_config_t: +// overwrite, read only, accumulate (read/modify/write) +enum effect_buffer_access_e { + EFFECT_BUFFER_ACCESS_WRITE, + EFFECT_BUFFER_ACCESS_READ, + EFFECT_BUFFER_ACCESS_ACCUMULATE + +}; + +// Values for bit field "mask" in buffer_config_t. If a bit is set, the corresponding field +// in buffer_config_t must be taken into account when executing the EFFECT_CMD_CONFIGURE command +#define EFFECT_CONFIG_BUFFER 0x0001 // buffer field must be taken into account +#define EFFECT_CONFIG_SMP_RATE 0x0002 // samplingRate field must be taken into account +#define EFFECT_CONFIG_CHANNELS 0x0004 // channels field must be taken into account +#define EFFECT_CONFIG_FORMAT 0x0008 // format field must be taken into account +#define EFFECT_CONFIG_ACC_MODE 0x0010 // accessMode field must be taken into account +#define EFFECT_CONFIG_PROVIDER 0x0020 // bufferProvider field must be taken into account +#define EFFECT_CONFIG_ALL (EFFECT_CONFIG_BUFFER | EFFECT_CONFIG_SMP_RATE | \ + EFFECT_CONFIG_CHANNELS | EFFECT_CONFIG_FORMAT | \ + EFFECT_CONFIG_ACC_MODE | EFFECT_CONFIG_PROVIDER) + + +// effect_config_s structure describes the format of the pCmdData argument of EFFECT_CMD_CONFIGURE +// command to configure audio parameters and buffers for effect engine input and output. +typedef struct effect_config_s { + buffer_config_t inputCfg; + buffer_config_t outputCfg;; +} effect_config_t; + + +// effect_param_s structure describes the format of the pCmdData argument of EFFECT_CMD_SET_PARAM +// command and pCmdData and pReplyData of EFFECT_CMD_GET_PARAM command. +// psize and vsize represent the actual size of parameter and value. +// +// NOTE: the start of value field inside the data field is always on a 32 bit boundary: +// +// +-----------+ +// | status | sizeof(int) +// +-----------+ +// | psize | sizeof(int) +// +-----------+ +// | vsize | sizeof(int) +// +-----------+ +// | | | | +// ~ parameter ~ > psize | +// | | | > ((psize - 1)/sizeof(int) + 1) * sizeof(int) +// +-----------+ | +// | padding | | +// +-----------+ +// | | | +// ~ value ~ > vsize +// | | | +// +-----------+ + +typedef struct effect_param_s { + int32_t status; // Transaction status (unused for command, used for reply) + uint32_t psize; // Parameter size + uint32_t vsize; // Value size + char data[]; // Start of Parameter + Value data +} effect_param_t; + + +///////////////////////////////////////////////// +// Effect library interface +///////////////////////////////////////////////// + +// An effect library is required to implement and expose the following functions +// to enable effect enumeration and instantiation. The name of these functions must be as +// specified here as the effect framework will get the function address with dlsym(): +// +// - effect_QueryNumberEffects_t EffectQueryNumberEffects; +// - effect_QueryEffect_t EffectQueryEffect; +// - effect_CreateEffect_t EffectCreate; +// - effect_ReleaseEffect_t EffectRelease; + + +//////////////////////////////////////////////////////////////////////////////// +// +// Function: EffectQueryNumberEffects +// +// Description: Returns the number of different effects exposed by the +// library. Each effect must have a unique effect uuid (see +// effect_descriptor_t). This function together with EffectQueryEffect() +// is used to enumerate all effects present in the library. +// +// Input/Output: +// pNumEffects: address where the number of effects should be returned. +// +// Output: +// returned value: 0 successful operation. +// -ENODEV library failed to initialize +// -EINVAL invalid pNumEffects +// *pNumEffects: updated with number of effects in library +// +//////////////////////////////////////////////////////////////////////////////// +typedef int32_t (*effect_QueryNumberEffects_t)(uint32_t *pNumEffects); + +//////////////////////////////////////////////////////////////////////////////// +// +// Function: EffectQueryEffect +// +// Description: Returns the descriptor of the effect engine which index is +// given as first argument. +// See effect_descriptor_t for details on effect descriptors. +// This function together with EffectQueryNumberEffects() is used to enumerate all +// effects present in the library. The enumeration sequence is: +// EffectQueryNumberEffects(&num_effects); +// for (i = 0; i < num_effects; i++) +// EffectQueryEffect(i,...); +// +// Input/Output: +// index: index of the effect +// pDescriptor: address where to return the effect descriptor. +// +// Output: +// returned value: 0 successful operation. +// -ENODEV library failed to initialize +// -EINVAL invalid pDescriptor or index +// -ENOSYS effect list has changed since last execution of +// EffectQueryNumberEffects() +// -ENOENT no more effect available +// *pDescriptor: updated with the effect descriptor. +// +//////////////////////////////////////////////////////////////////////////////// +typedef int32_t (*effect_QueryEffect_t)(uint32_t index, + effect_descriptor_t *pDescriptor); + +//////////////////////////////////////////////////////////////////////////////// +// +// Function: EffectCreate +// +// Description: Creates an effect engine of the specified type and returns an +// effect control interface on this engine. The function will allocate the +// resources for an instance of the requested effect engine and return +// a handle on the effect control interface. +// +// Input: +// uuid: pointer to the effect uuid. +// sessionId: audio session to which this effect instance will be attached. All effects +// created with the same session ID are connected in series and process the same signal +// stream. Knowing that two effects are part of the same effect chain can help the +// library implement some kind of optimizations. +// ioId: identifies the output or input stream this effect is directed to at audio HAL. +// For future use especially with tunneled HW accelerated effects +// +// Input/Output: +// pInterface: address where to return the effect interface. +// +// Output: +// returned value: 0 successful operation. +// -ENODEV library failed to initialize +// -EINVAL invalid pEffectUuid or pInterface +// -ENOENT no effect with this uuid found +// *pInterface: updated with the effect interface handle. +// +//////////////////////////////////////////////////////////////////////////////// +typedef int32_t (*effect_CreateEffect_t)(effect_uuid_t *uuid, + int32_t sessionId, + int32_t ioId, + effect_interface_t *pInterface); + +//////////////////////////////////////////////////////////////////////////////// +// +// Function: EffectRelease +// +// Description: Releases the effect engine whose handle is given as argument. +// All resources allocated to this particular instance of the effect are +// released. +// +// Input: +// interface: handle on the effect interface to be released. +// +// Output: +// returned value: 0 successful operation. +// -ENODEV library failed to initialize +// -EINVAL invalid interface handle +// +//////////////////////////////////////////////////////////////////////////////// +typedef int32_t (*effect_ReleaseEffect_t)(effect_interface_t interface); + + +#if __cplusplus +} // extern "C" +#endif + + +#endif /*ANDROID_EFFECTAPI_H_*/ diff --git a/dom/system/gonk/android_audio/IAudioFlinger.h b/dom/system/gonk/android_audio/IAudioFlinger.h new file mode 100644 index 000000000..b10d3ab93 --- /dev/null +++ b/dom/system/gonk/android_audio/IAudioFlinger.h @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_IAUDIOFLINGER_H +#define ANDROID_IAUDIOFLINGER_H + +#include <stdint.h> +#include <sys/types.h> +#include <unistd.h> + +#include <utils/RefBase.h> +#include <utils/Errors.h> +#include <binder/IInterface.h> +#include "IAudioTrack.h" +#include "IAudioRecord.h" +#include "IAudioFlingerClient.h" +#include "EffectApi.h" +#include "IEffect.h" +#include "IEffectClient.h" +#include <utils/String8.h> + +namespace android { + +// ---------------------------------------------------------------------------- + +class IAudioFlinger : public IInterface +{ +public: + DECLARE_META_INTERFACE(AudioFlinger); + + /* create an audio track and registers it with AudioFlinger. + * return null if the track cannot be created. + */ + virtual sp<IAudioTrack> createTrack( + pid_t pid, + int streamType, + uint32_t sampleRate, + int format, + int channelCount, + int frameCount, + uint32_t flags, + const sp<IMemory>& sharedBuffer, + int output, + int *sessionId, + status_t *status) = 0; + + virtual sp<IAudioRecord> openRecord( + pid_t pid, + int input, + uint32_t sampleRate, + int format, + int channelCount, + int frameCount, + uint32_t flags, + int *sessionId, + status_t *status) = 0; + + /* query the audio hardware state. This state never changes, + * and therefore can be cached. + */ + virtual uint32_t sampleRate(int output) const = 0; + virtual int channelCount(int output) const = 0; + virtual int format(int output) const = 0; + virtual size_t frameCount(int output) const = 0; + virtual uint32_t latency(int output) const = 0; + + /* set/get the audio hardware state. This will probably be used by + * the preference panel, mostly. + */ + virtual status_t setMasterVolume(float value) = 0; + virtual status_t setMasterMute(bool muted) = 0; + + virtual float masterVolume() const = 0; + virtual bool masterMute() const = 0; + + /* set/get stream type state. This will probably be used by + * the preference panel, mostly. + */ + virtual status_t setStreamVolume(int stream, float value, int output) = 0; + virtual status_t setStreamMute(int stream, bool muted) = 0; + + virtual float streamVolume(int stream, int output) const = 0; + virtual bool streamMute(int stream) const = 0; + + // set audio mode + virtual status_t setMode(int mode) = 0; + + // mic mute/state + virtual status_t setMicMute(bool state) = 0; + virtual bool getMicMute() const = 0; + + // is any track active on this stream? + virtual bool isStreamActive(int stream) const = 0; + + virtual status_t setParameters(int ioHandle, const String8& keyValuePairs) = 0; + virtual String8 getParameters(int ioHandle, const String8& keys) = 0; + + // register a current process for audio output change notifications + virtual void registerClient(const sp<IAudioFlingerClient>& client) = 0; + + // retrieve the audio recording buffer size + virtual size_t getInputBufferSize(uint32_t sampleRate, int format, int channelCount) = 0; + + virtual int openOutput(uint32_t *pDevices, + uint32_t *pSamplingRate, + uint32_t *pFormat, + uint32_t *pChannels, + uint32_t *pLatencyMs, + uint32_t flags) = 0; + virtual int openDuplicateOutput(int output1, int output2) = 0; + virtual status_t closeOutput(int output) = 0; + virtual status_t suspendOutput(int output) = 0; + virtual status_t restoreOutput(int output) = 0; + + virtual int openInput(uint32_t *pDevices, + uint32_t *pSamplingRate, + uint32_t *pFormat, + uint32_t *pChannels, + uint32_t acoustics) = 0; + virtual status_t closeInput(int input) = 0; + + virtual status_t setStreamOutput(uint32_t stream, int output) = 0; + + virtual status_t setVoiceVolume(float volume) = 0; + + virtual status_t getRenderPosition(uint32_t *halFrames, uint32_t *dspFrames, int output) = 0; + + virtual unsigned int getInputFramesLost(int ioHandle) = 0; + + virtual int newAudioSessionId() = 0; + + virtual status_t loadEffectLibrary(const char *libPath, int *handle) = 0; + + virtual status_t unloadEffectLibrary(int handle) = 0; + + virtual status_t queryNumberEffects(uint32_t *numEffects) = 0; + + virtual status_t queryEffect(uint32_t index, effect_descriptor_t *pDescriptor) = 0; + + virtual status_t getEffectDescriptor(effect_uuid_t *pEffectUUID, effect_descriptor_t *pDescriptor) = 0; + + virtual sp<IEffect> createEffect(pid_t pid, + effect_descriptor_t *pDesc, + const sp<IEffectClient>& client, + int32_t priority, + int output, + int sessionId, + status_t *status, + int *id, + int *enabled) = 0; + + virtual status_t moveEffects(int session, int srcOutput, int dstOutput) = 0; +}; + + +// ---------------------------------------------------------------------------- + +class BnAudioFlinger : public BnInterface<IAudioFlinger> +{ +public: + virtual status_t onTransact( uint32_t code, + const Parcel& data, + Parcel* reply, + uint32_t flags = 0); +}; + +// ---------------------------------------------------------------------------- + +}; // namespace android + +#endif // ANDROID_IAUDIOFLINGER_H diff --git a/dom/system/gonk/android_audio/IAudioFlingerClient.h b/dom/system/gonk/android_audio/IAudioFlingerClient.h new file mode 100644 index 000000000..aa0cdcff1 --- /dev/null +++ b/dom/system/gonk/android_audio/IAudioFlingerClient.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2009 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. + */ + +#ifndef ANDROID_IAUDIOFLINGERCLIENT_H +#define ANDROID_IAUDIOFLINGERCLIENT_H + + +#include <utils/RefBase.h> +#include <binder/IInterface.h> +#include <utils/KeyedVector.h> + +namespace android { + +// ---------------------------------------------------------------------------- + +class IAudioFlingerClient : public IInterface +{ +public: + DECLARE_META_INTERFACE(AudioFlingerClient); + + // Notifies a change of audio input/output configuration. + virtual void ioConfigChanged(int event, int ioHandle, void *param2) = 0; + +}; + + +// ---------------------------------------------------------------------------- + +class BnAudioFlingerClient : public BnInterface<IAudioFlingerClient> +{ +public: + virtual status_t onTransact( uint32_t code, + const Parcel& data, + Parcel* reply, + uint32_t flags = 0); +}; + +// ---------------------------------------------------------------------------- + +}; // namespace android + +#endif // ANDROID_IAUDIOFLINGERCLIENT_H diff --git a/dom/system/gonk/android_audio/IAudioRecord.h b/dom/system/gonk/android_audio/IAudioRecord.h new file mode 100644 index 000000000..46735def2 --- /dev/null +++ b/dom/system/gonk/android_audio/IAudioRecord.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef IAUDIORECORD_H_ +#define IAUDIORECORD_H_ + +#include <stdint.h> +#include <sys/types.h> + +#include <utils/RefBase.h> +#include <utils/Errors.h> +#include <binder/IInterface.h> +#include <binder/IMemory.h> + + +namespace android { + +// ---------------------------------------------------------------------------- + +class IAudioRecord : public IInterface +{ +public: + DECLARE_META_INTERFACE(AudioRecord); + + /* After it's created the track is not active. Call start() to + * make it active. If set, the callback will start being called. + */ + virtual status_t start() = 0; + + /* Stop a track. If set, the callback will cease being called and + * obtainBuffer will return an error. Buffers that are already released + * will be processed, unless flush() is called. + */ + virtual void stop() = 0; + + /* get this tracks control block */ + virtual sp<IMemory> getCblk() const = 0; +}; + +// ---------------------------------------------------------------------------- + +class BnAudioRecord : public BnInterface<IAudioRecord> +{ +public: + virtual status_t onTransact( uint32_t code, + const Parcel& data, + Parcel* reply, + uint32_t flags = 0); +}; + +// ---------------------------------------------------------------------------- + +}; // namespace android + +#endif /*IAUDIORECORD_H_*/ diff --git a/dom/system/gonk/android_audio/IAudioTrack.h b/dom/system/gonk/android_audio/IAudioTrack.h new file mode 100644 index 000000000..47d530be5 --- /dev/null +++ b/dom/system/gonk/android_audio/IAudioTrack.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_IAUDIOTRACK_H +#define ANDROID_IAUDIOTRACK_H + +#include <stdint.h> +#include <sys/types.h> + +#include <utils/RefBase.h> +#include <utils/Errors.h> +#include <binder/IInterface.h> +#include <binder/IMemory.h> + + +namespace android { + +// ---------------------------------------------------------------------------- + +class IAudioTrack : public IInterface +{ +public: + DECLARE_META_INTERFACE(AudioTrack); + + /* After it's created the track is not active. Call start() to + * make it active. If set, the callback will start being called. + */ + virtual status_t start() = 0; + + /* Stop a track. If set, the callback will cease being called and + * obtainBuffer will return an error. Buffers that are already released + * will be processed, unless flush() is called. + */ + virtual void stop() = 0; + + /* flush a stopped track. All pending buffers are discarded. + * This function has no effect if the track is not stoped. + */ + virtual void flush() = 0; + + /* mute or unmutes this track. + * While mutted, the callback, if set, is still called. + */ + virtual void mute(bool) = 0; + + /* Pause a track. If set, the callback will cease being called and + * obtainBuffer will return an error. Buffers that are already released + * will be processed, unless flush() is called. + */ + virtual void pause() = 0; + + /* Attach track auxiliary output to specified effect. Use effectId = 0 + * to detach track from effect. + */ + virtual status_t attachAuxEffect(int effectId) = 0; + + /* get this tracks control block */ + virtual sp<IMemory> getCblk() const = 0; +}; + +// ---------------------------------------------------------------------------- + +class BnAudioTrack : public BnInterface<IAudioTrack> +{ +public: + virtual status_t onTransact( uint32_t code, + const Parcel& data, + Parcel* reply, + uint32_t flags = 0); +}; + +// ---------------------------------------------------------------------------- + +}; // namespace android + +#endif // ANDROID_IAUDIOTRACK_H diff --git a/dom/system/gonk/android_audio/IEffect.h b/dom/system/gonk/android_audio/IEffect.h new file mode 100644 index 000000000..ff04869e0 --- /dev/null +++ b/dom/system/gonk/android_audio/IEffect.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010 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. + */ + +#ifndef ANDROID_IEFFECT_H +#define ANDROID_IEFFECT_H + +#include <utils/RefBase.h> +#include <binder/IInterface.h> +#include <binder/Parcel.h> +#include <binder/IMemory.h> + +namespace android { + +class IEffect: public IInterface +{ +public: + DECLARE_META_INTERFACE(Effect); + + virtual status_t enable() = 0; + + virtual status_t disable() = 0; + + virtual status_t command(uint32_t cmdCode, + uint32_t cmdSize, + void *pCmdData, + uint32_t *pReplySize, + void *pReplyData) = 0; + + virtual void disconnect() = 0; + + virtual sp<IMemory> getCblk() const = 0; +}; + +// ---------------------------------------------------------------------------- + +class BnEffect: public BnInterface<IEffect> +{ +public: + virtual status_t onTransact( uint32_t code, + const Parcel& data, + Parcel* reply, + uint32_t flags = 0); +}; + +}; // namespace android + +#endif // ANDROID_IEFFECT_H diff --git a/dom/system/gonk/android_audio/IEffectClient.h b/dom/system/gonk/android_audio/IEffectClient.h new file mode 100644 index 000000000..2f78c98f1 --- /dev/null +++ b/dom/system/gonk/android_audio/IEffectClient.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 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. + */ + +#ifndef ANDROID_IEFFECTCLIENT_H +#define ANDROID_IEFFECTCLIENT_H + +#include <utils/RefBase.h> +#include <binder/IInterface.h> +#include <binder/Parcel.h> +#include <binder/IMemory.h> + +namespace android { + +class IEffectClient: public IInterface +{ +public: + DECLARE_META_INTERFACE(EffectClient); + + virtual void controlStatusChanged(bool controlGranted) = 0; + virtual void enableStatusChanged(bool enabled) = 0; + virtual void commandExecuted(uint32_t cmdCode, + uint32_t cmdSize, + void *pCmdData, + uint32_t replySize, + void *pReplyData) = 0; +}; + +// ---------------------------------------------------------------------------- + +class BnEffectClient: public BnInterface<IEffectClient> +{ +public: + virtual status_t onTransact( uint32_t code, + const Parcel& data, + Parcel* reply, + uint32_t flags = 0); +}; + +}; // namespace android + +#endif // ANDROID_IEFFECTCLIENT_H diff --git a/dom/system/gonk/moz.build b/dom/system/gonk/moz.build new file mode 100644 index 000000000..229baaab4 --- /dev/null +++ b/dom/system/gonk/moz.build @@ -0,0 +1,107 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# Copyright 2013 Mozilla Foundation and Mozilla contributors +# +# 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. + +XPIDL_SOURCES += [ + 'nsIAudioManager.idl', + 'nsINetworkInterface.idl', + 'nsINetworkInterfaceListService.idl', + 'nsINetworkManager.idl', + 'nsINetworkService.idl', + 'nsINetworkWorker.idl', + 'nsISystemWorkerManager.idl', + 'nsITetheringService.idl', + 'nsIVolume.idl', + 'nsIVolumeMountLock.idl', + 'nsIVolumeService.idl', + 'nsIVolumeStat.idl', + 'nsIWorkerHolder.idl', +] + +XPIDL_MODULE = 'dom_system_gonk' + +EXPORTS += [ + 'GeolocationUtil.h', + 'GonkGPSGeolocationProvider.h', + 'nsVolume.h', + 'nsVolumeService.h', + 'SystemProperty.h', +] +UNIFIED_SOURCES += [ + 'AudioChannelManager.cpp', + 'AudioManager.cpp', + 'AutoMounter.cpp', + 'AutoMounterSetting.cpp', + 'GeolocationUtil.cpp', + 'GonkGPSGeolocationProvider.cpp', + 'MozMtpDatabase.cpp', + 'MozMtpServer.cpp', + 'MozMtpStorage.cpp', + 'NetIdManager.cpp', + 'NetworkUtils.cpp', + 'NetworkWorker.cpp', + 'nsVolume.cpp', + 'nsVolumeMountLock.cpp', + 'nsVolumeService.cpp', + 'nsVolumeStat.cpp', + 'OpenFileFinder.cpp', + 'SystemProperty.cpp', + 'SystemWorkerManager.cpp', + 'TimeZoneSettingObserver.cpp', + 'Volume.cpp', + 'VolumeCommand.cpp', + 'VolumeManager.cpp', + 'VolumeServiceIOThread.cpp', + 'VolumeServiceTest.cpp', +] + +if CONFIG['ANDROID_VERSION'] >= '17': + LOCAL_INCLUDES += ['%' + '%s/frameworks/av/media/mtp' % CONFIG['ANDROID_SOURCE']] +else: + LOCAL_INCLUDES += ['%' + '%s/frameworks/base/media/mtp' % CONFIG['ANDROID_SOURCE']] + +if CONFIG['ENABLE_TESTS']: + XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell.ini'] + +EXTRA_COMPONENTS += [ + 'NetworkInterfaceListService.js', + 'NetworkInterfaceListService.manifest', + 'NetworkManager.js', + 'NetworkManager.manifest', + 'NetworkService.js', + 'NetworkService.manifest', + 'TetheringService.js', + 'TetheringService.manifest', +] +EXTRA_JS_MODULES += [ + 'systemlibs.js', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +DEFINES['HAVE_ANDROID_OS'] = True + +LOCAL_INCLUDES += [ + '/dom/base', + '/dom/bluetooth/common', + '/dom/geolocation', + '/dom/wifi', +] + +FINAL_LIBRARY = 'xul' + +FINAL_TARGET_FILES.modules.workers += [ + 'worker_buf.js', +] diff --git a/dom/system/gonk/mozstumbler/MozStumbler.cpp b/dom/system/gonk/mozstumbler/MozStumbler.cpp new file mode 100644 index 000000000..61e09e705 --- /dev/null +++ b/dom/system/gonk/mozstumbler/MozStumbler.cpp @@ -0,0 +1,426 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MozStumbler.h" +#include "nsDataHashtable.h" +#include "nsGeoPosition.h" +#include "nsNetCID.h" +#include "nsPrintfCString.h" +#include "StumblerLogging.h" +#include "WriteStumbleOnThread.h" +#include "../GeolocationUtil.h" + +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIMobileConnectionInfo.h" +#include "nsIMobileConnectionService.h" +#include "nsIMobileCellInfo.h" +#include "nsIMobileNetworkInfo.h" +#include "nsINetworkInterface.h" +#include "nsIRadioInterfaceLayer.h" + +using namespace mozilla; +using namespace mozilla::dom; + + +NS_IMPL_ISUPPORTS(StumblerInfo, nsICellInfoListCallback, nsIWifiScanResultsReady) + +class RequestCellInfoEvent : public Runnable { +public: + RequestCellInfoEvent(StumblerInfo *callback) + : mRequestCallback(callback) + {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + // Get Cell Info + nsCOMPtr<nsIMobileConnectionService> service = + do_GetService(NS_MOBILE_CONNECTION_SERVICE_CONTRACTID); + + if (!service) { + STUMBLER_ERR("Stumbler-can not get nsIMobileConnectionService \n"); + return NS_OK; + } + nsCOMPtr<nsIMobileConnection> connection; + uint32_t numberOfRilServices = 1, cellInfoNum = 0; + + service->GetNumItems(&numberOfRilServices); + for (uint32_t rilNum = 0; rilNum < numberOfRilServices; rilNum++) { + service->GetItemByServiceId(rilNum /* Client Id */, getter_AddRefs(connection)); + if (!connection) { + STUMBLER_ERR("Stumbler-can not get nsIMobileConnection by ServiceId %d \n", rilNum); + } else { + cellInfoNum++; + connection->GetCellInfoList(mRequestCallback); + } + } + mRequestCallback->SetCellInfoResponsesExpected(cellInfoNum); + + // Get Wifi AP Info + nsCOMPtr<nsIInterfaceRequestor> ir = do_GetService("@mozilla.org/telephony/system-worker-manager;1"); + nsCOMPtr<nsIWifi> wifi = do_GetInterface(ir); + if (!wifi) { + mRequestCallback->SetWifiInfoResponseReceived(); + STUMBLER_ERR("Stumbler-can not get nsIWifi interface\n"); + return NS_OK; + } + wifi->GetWifiScanResults(mRequestCallback); + return NS_OK; + } +private: + RefPtr<StumblerInfo> mRequestCallback; +}; + +void +MozStumble(nsGeoPosition* position) +{ + if (WriteStumbleOnThread::IsFileWaitingForUpload()) { + nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + // Knowing that file is waiting to upload, and no collection will take place, + // just trigger the thread with an empty string. + nsCOMPtr<nsIRunnable> event = new WriteStumbleOnThread(EmptyCString()); + target->Dispatch(event, NS_DISPATCH_NORMAL); + return; + } + + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + position->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return; + } + + double latitude, longitude; + coords->GetLatitude(&latitude); + coords->GetLongitude(&longitude); + + const double kMinChangeInMeters = 30; + static int64_t lastTime_ms = 0; + static double sLastLat = 0; + static double sLastLon = 0; + double delta = -1.0; + int64_t timediff = (PR_Now() / PR_USEC_PER_MSEC) - lastTime_ms; + + if (0 != sLastLon || 0 != sLastLat) { + delta = CalculateDeltaInMeter(latitude, longitude, sLastLat, sLastLon); + } + STUMBLER_DBG("Stumbler-Location. [%f , %f] time_diff:%lld, delta : %f\n", + longitude, latitude, timediff, delta); + + // Consecutive GPS locations must be 30 meters and 3 seconds apart + if (lastTime_ms == 0 || ((timediff >= STUMBLE_INTERVAL_MS) && (delta > kMinChangeInMeters))){ + lastTime_ms = (PR_Now() / PR_USEC_PER_MSEC); + sLastLat = latitude; + sLastLon = longitude; + RefPtr<StumblerInfo> requestCallback = new StumblerInfo(position); + RefPtr<RequestCellInfoEvent> runnable = new RequestCellInfoEvent(requestCallback); + NS_DispatchToMainThread(runnable); + } else { + STUMBLER_DBG("Stumbler-GPS locations less than 30 meters and 3 seconds. Ignore!\n"); + } +} + +void +StumblerInfo::SetWifiInfoResponseReceived() +{ + mIsWifiInfoResponseReceived = true; + + if (mIsWifiInfoResponseReceived && mCellInfoResponsesReceived == mCellInfoResponsesExpected) { + STUMBLER_DBG("Call DumpStumblerInfo from SetWifiInfoResponseReceived\n"); + DumpStumblerInfo(); + } +} + +void +StumblerInfo::SetCellInfoResponsesExpected(uint8_t count) +{ + mCellInfoResponsesExpected = count; + STUMBLER_DBG("SetCellInfoNum (%d)\n", count); + + if (mIsWifiInfoResponseReceived && mCellInfoResponsesReceived == mCellInfoResponsesExpected) { + STUMBLER_DBG("Call DumpStumblerInfo from SetCellInfoResponsesExpected\n"); + DumpStumblerInfo(); + } +} + + +#define TEXT_LAT NS_LITERAL_CSTRING("latitude") +#define TEXT_LON NS_LITERAL_CSTRING("longitude") +#define TEXT_ACC NS_LITERAL_CSTRING("accuracy") +#define TEXT_ALT NS_LITERAL_CSTRING("altitude") +#define TEXT_ALTACC NS_LITERAL_CSTRING("altitudeAccuracy") +#define TEXT_HEAD NS_LITERAL_CSTRING("heading") +#define TEXT_SPD NS_LITERAL_CSTRING("speed") + +nsresult +StumblerInfo::LocationInfoToString(nsACString& aLocDesc) +{ + nsCOMPtr<nsIDOMGeoPositionCoords> coords; + mPosition->GetCoords(getter_AddRefs(coords)); + if (!coords) { + return NS_ERROR_FAILURE; + } + + nsDataHashtable<nsCStringHashKey, double> info; + + double val; + coords->GetLatitude(&val); + info.Put(TEXT_LAT, val); + coords->GetLongitude(&val); + info.Put(TEXT_LON, val); + coords->GetAccuracy(&val); + info.Put(TEXT_ACC, val); + coords->GetAltitude(&val); + info.Put(TEXT_ALT, val); + coords->GetAltitudeAccuracy(&val); + info.Put(TEXT_ALTACC, val); + coords->GetHeading(&val); + info.Put(TEXT_HEAD, val); + coords->GetSpeed(&val); + info.Put(TEXT_SPD, val); + + for (auto it = info.Iter(); !it.Done(); it.Next()) { + const nsACString& key = it.Key(); + val = it.UserData(); + if (!IsNaN(val)) { + aLocDesc += nsPrintfCString("\"%s\":%f,", key.BeginReading(), val); + } + } + + aLocDesc += nsPrintfCString("\"timestamp\":%lld,", PR_Now() / PR_USEC_PER_MSEC).get(); + return NS_OK; +} + +#define TEXT_RADIOTYPE NS_LITERAL_CSTRING("radioType") +#define TEXT_MCC NS_LITERAL_CSTRING("mobileCountryCode") +#define TEXT_MNC NS_LITERAL_CSTRING("mobileNetworkCode") +#define TEXT_LAC NS_LITERAL_CSTRING("locationAreaCode") +#define TEXT_CID NS_LITERAL_CSTRING("cellId") +#define TEXT_PSC NS_LITERAL_CSTRING("psc") +#define TEXT_STRENGTH_ASU NS_LITERAL_CSTRING("asu") +#define TEXT_STRENGTH_DBM NS_LITERAL_CSTRING("signalStrength") +#define TEXT_REGISTERED NS_LITERAL_CSTRING("serving") +#define TEXT_TIMEING_ADVANCE NS_LITERAL_CSTRING("timingAdvance") + +template <class T> void +ExtractCommonNonCDMACellInfoItems(nsCOMPtr<T>& cell, nsDataHashtable<nsCStringHashKey, int32_t>& info) +{ + int32_t mcc, mnc, cid, sig; + + cell->GetMcc(&mcc); + cell->GetMnc(&mnc); + cell->GetCid(&cid); + cell->GetSignalStrength(&sig); + + info.Put(TEXT_MCC, mcc); + info.Put(TEXT_MNC, mnc); + info.Put(TEXT_CID, cid); + info.Put(TEXT_STRENGTH_ASU, sig); +} + +void +StumblerInfo::CellNetworkInfoToString(nsACString& aCellDesc) +{ + aCellDesc += "\"cellTowers\": ["; + + for (uint32_t idx = 0; idx < mCellInfo.Length() ; idx++) { + const char* radioType = 0; + int32_t type; + mCellInfo[idx]->GetType(&type); + bool registered; + mCellInfo[idx]->GetRegistered(®istered); + if (idx) { + aCellDesc += ",{"; + } else { + aCellDesc += "{"; + } + + STUMBLER_DBG("type=%d\n", type); + + nsDataHashtable<nsCStringHashKey, int32_t> info; + info.Put(TEXT_REGISTERED, registered); + + if(type == nsICellInfo::CELL_INFO_TYPE_GSM) { + radioType = "gsm"; + nsCOMPtr<nsIGsmCellInfo> gsmCellInfo = do_QueryInterface(mCellInfo[idx]); + ExtractCommonNonCDMACellInfoItems(gsmCellInfo, info); + int32_t lac; + gsmCellInfo->GetLac(&lac); + info.Put(TEXT_LAC, lac); + } else if (type == nsICellInfo::CELL_INFO_TYPE_WCDMA) { + radioType = "wcdma"; + nsCOMPtr<nsIWcdmaCellInfo> wcdmaCellInfo = do_QueryInterface(mCellInfo[idx]); + ExtractCommonNonCDMACellInfoItems(wcdmaCellInfo, info); + int32_t lac, psc; + wcdmaCellInfo->GetLac(&lac); + wcdmaCellInfo->GetPsc(&psc); + info.Put(TEXT_LAC, lac); + info.Put(TEXT_PSC, psc); + } else if (type == nsICellInfo::CELL_INFO_TYPE_CDMA) { + radioType = "cdma"; + nsCOMPtr<nsICdmaCellInfo> cdmaCellInfo = do_QueryInterface(mCellInfo[idx]); + int32_t mnc, lac, cid, sig; + cdmaCellInfo->GetSystemId(&mnc); + cdmaCellInfo->GetNetworkId(&lac); + cdmaCellInfo->GetBaseStationId(&cid); + info.Put(TEXT_MNC, mnc); + info.Put(TEXT_LAC, lac); + info.Put(TEXT_CID, cid); + + cdmaCellInfo->GetEvdoDbm(&sig); + if (sig < 0 || sig == nsICellInfo::UNKNOWN_VALUE) { + cdmaCellInfo->GetCdmaDbm(&sig); + } + if (sig > -1 && sig != nsICellInfo::UNKNOWN_VALUE) { + sig *= -1; + info.Put(TEXT_STRENGTH_DBM, sig); + } + } else if (type == nsICellInfo::CELL_INFO_TYPE_LTE) { + radioType = "lte"; + nsCOMPtr<nsILteCellInfo> lteCellInfo = do_QueryInterface(mCellInfo[idx]); + ExtractCommonNonCDMACellInfoItems(lteCellInfo, info); + int32_t lac, timingAdvance, pcid, rsrp; + lteCellInfo->GetTac(&lac); + lteCellInfo->GetTimingAdvance(&timingAdvance); + lteCellInfo->GetPcid(&pcid); + lteCellInfo->GetRsrp(&rsrp); + info.Put(TEXT_LAC, lac); + info.Put(TEXT_TIMEING_ADVANCE, timingAdvance); + info.Put(TEXT_PSC, pcid); + if (rsrp != nsICellInfo::UNKNOWN_VALUE) { + info.Put(TEXT_STRENGTH_DBM, rsrp * -1); + } + } + + aCellDesc += nsPrintfCString("\"%s\":\"%s\"", TEXT_RADIOTYPE.get(), radioType); + for (auto it = info.Iter(); !it.Done(); it.Next()) { + const nsACString& key = it.Key(); + int32_t value = it.UserData(); + if (value != nsICellInfo::UNKNOWN_VALUE) { + aCellDesc += nsPrintfCString(",\"%s\":%d", key.BeginReading(), value); + } + } + + aCellDesc += "}"; + } + aCellDesc += "]"; +} + +void +StumblerInfo::DumpStumblerInfo() +{ + if (!mIsWifiInfoResponseReceived || mCellInfoResponsesReceived != mCellInfoResponsesExpected) { + STUMBLER_DBG("CellInfoReceived=%d (Expected=%d), WifiInfoResponseReceived=%d\n", + mCellInfoResponsesReceived, mCellInfoResponsesExpected, mIsWifiInfoResponseReceived); + return; + } + mIsWifiInfoResponseReceived = false; + mCellInfoResponsesReceived = 0; + + nsAutoCString desc; + nsresult rv = LocationInfoToString(desc); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("LocationInfoToString failed, skip this dump"); + return; + } + + CellNetworkInfoToString(desc); + desc += mWifiDesc; + + STUMBLER_DBG("dispatch write event to thread\n"); + nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + + nsCOMPtr<nsIRunnable> event = new WriteStumbleOnThread(desc); + target->Dispatch(event, NS_DISPATCH_NORMAL); +} + +NS_IMETHODIMP +StumblerInfo::NotifyGetCellInfoList(uint32_t count, nsICellInfo** aCellInfos) +{ + MOZ_ASSERT(NS_IsMainThread()); + STUMBLER_DBG("There are %d cellinfo in the result\n", count); + + for (uint32_t i = 0; i < count; i++) { + mCellInfo.AppendElement(aCellInfos[i]); + } + mCellInfoResponsesReceived++; + DumpStumblerInfo(); + return NS_OK; +} + +NS_IMETHODIMP StumblerInfo::NotifyGetCellInfoListFailed(const nsAString& error) +{ + MOZ_ASSERT(NS_IsMainThread()); + mCellInfoResponsesReceived++; + STUMBLER_ERR("NotifyGetCellInfoListFailedm CellInfoReadyNum=%d, mCellInfoResponsesExpected=%d, mIsWifiInfoResponseReceived=%d", + mCellInfoResponsesReceived, mCellInfoResponsesExpected, mIsWifiInfoResponseReceived); + DumpStumblerInfo(); + return NS_OK; +} + +NS_IMETHODIMP +StumblerInfo::Onready(uint32_t count, nsIWifiScanResult** results) +{ + MOZ_ASSERT(NS_IsMainThread()); + STUMBLER_DBG("There are %d wifiAPinfo in the result\n",count); + + mWifiDesc += ",\"wifiAccessPoints\": ["; + bool firstItem = true; + for (uint32_t i = 0 ; i < count ; i++) { + nsString ssid; + results[i]->GetSsid(ssid); + if (ssid.IsEmpty()) { + STUMBLER_DBG("no ssid, skip this AP\n"); + continue; + } + + if (ssid.Length() >= 6) { + if (StringEndsWith(ssid, NS_LITERAL_STRING("_nomap"))) { + STUMBLER_DBG("end with _nomap. skip this AP(ssid :%s)\n", ssid.get()); + continue; + } + } + + if (firstItem) { + mWifiDesc += "{"; + firstItem = false; + } else { + mWifiDesc += ",{"; + } + + // mac address + nsString bssid; + results[i]->GetBssid(bssid); + // 00:00:00:00:00:00 --> 000000000000 + bssid.StripChars(":"); + mWifiDesc += "\"macAddress\":\""; + mWifiDesc += NS_ConvertUTF16toUTF8(bssid); + + uint32_t signal; + results[i]->GetSignalStrength(&signal); + mWifiDesc += "\",\"signalStrength\":"; + mWifiDesc.AppendInt(signal); + + mWifiDesc += "}"; + } + mWifiDesc += "]"; + + mIsWifiInfoResponseReceived = true; + DumpStumblerInfo(); + return NS_OK; +} + +NS_IMETHODIMP +StumblerInfo::Onfailure() +{ + MOZ_ASSERT(NS_IsMainThread()); + STUMBLER_ERR("GetWifiScanResults Onfailure\n"); + mIsWifiInfoResponseReceived = true; + DumpStumblerInfo(); + return NS_OK; +} + diff --git a/dom/system/gonk/mozstumbler/MozStumbler.h b/dom/system/gonk/mozstumbler/MozStumbler.h new file mode 100644 index 000000000..41ee4e5e1 --- /dev/null +++ b/dom/system/gonk/mozstumbler/MozStumbler.h @@ -0,0 +1,47 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_mozstumbler_h__ +#define mozilla_system_mozstumbler_h__ + +#include "nsIDOMEventTarget.h" +#include "nsICellInfo.h" +#include "nsIWifi.h" + +#define STUMBLE_INTERVAL_MS 3000 + +class nsGeoPosition; + +void MozStumble(nsGeoPosition* position); + +class StumblerInfo final : public nsICellInfoListCallback, + public nsIWifiScanResultsReady +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICELLINFOLISTCALLBACK + NS_DECL_NSIWIFISCANRESULTSREADY + + explicit StumblerInfo(nsGeoPosition* position) + : mPosition(position), mCellInfoResponsesExpected(0), mCellInfoResponsesReceived(0), mIsWifiInfoResponseReceived(0) + {} + void SetWifiInfoResponseReceived(); + void SetCellInfoResponsesExpected(uint8_t count); + +private: + ~StumblerInfo() {} + void DumpStumblerInfo(); + nsresult LocationInfoToString(nsACString& aLocDesc); + void CellNetworkInfoToString(nsACString& aCellDesc); + nsTArray<RefPtr<nsICellInfo>> mCellInfo; + nsCString mWifiDesc; + RefPtr<nsGeoPosition> mPosition; + int mCellInfoResponsesExpected; + int mCellInfoResponsesReceived; + bool mIsWifiInfoResponseReceived; +}; +#endif // mozilla_system_mozstumbler_h__ + diff --git a/dom/system/gonk/mozstumbler/StumblerLogging.cpp b/dom/system/gonk/mozstumbler/StumblerLogging.cpp new file mode 100644 index 000000000..acf23b3b1 --- /dev/null +++ b/dom/system/gonk/mozstumbler/StumblerLogging.cpp @@ -0,0 +1,13 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "StumblerLogging.h" + +mozilla::LogModule* GetLog() +{ + static mozilla::LazyLogModule log("mozstumbler"); + return log; +} diff --git a/dom/system/gonk/mozstumbler/StumblerLogging.h b/dom/system/gonk/mozstumbler/StumblerLogging.h new file mode 100644 index 000000000..038f44f8f --- /dev/null +++ b/dom/system/gonk/mozstumbler/StumblerLogging.h @@ -0,0 +1,18 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef STUMBLERLOGGING_H +#define STUMBLERLOGGING_H + +#include "mozilla/Logging.h" + +mozilla::LogModule* GetLog(); + +#define STUMBLER_DBG(arg, ...) MOZ_LOG(GetLog(), mozilla::LogLevel::Debug, ("STUMBLER - %s: " arg, __func__, ##__VA_ARGS__)) +#define STUMBLER_LOG(arg, ...) MOZ_LOG(GetLog(), mozilla::LogLevel::Info, ("STUMBLER - %s: " arg, __func__, ##__VA_ARGS__)) +#define STUMBLER_ERR(arg, ...) MOZ_LOG(GetLog(), mozilla::LogLevel::Error, ("STUMBLER -%s: " arg, __func__, ##__VA_ARGS__)) + +#endif diff --git a/dom/system/gonk/mozstumbler/UploadStumbleRunnable.cpp b/dom/system/gonk/mozstumbler/UploadStumbleRunnable.cpp new file mode 100644 index 000000000..d97aa9712 --- /dev/null +++ b/dom/system/gonk/mozstumbler/UploadStumbleRunnable.cpp @@ -0,0 +1,151 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "UploadStumbleRunnable.h" +#include "StumblerLogging.h" +#include "mozilla/dom/Event.h" +#include "nsIInputStream.h" +#include "nsIScriptSecurityManager.h" +#include "nsIURLFormatter.h" +#include "nsIXMLHttpRequest.h" +#include "nsNetUtil.h" +#include "nsVariant.h" + +UploadStumbleRunnable::UploadStumbleRunnable(nsIInputStream* aUploadData) +: mUploadInputStream(aUploadData) +{ +} + +NS_IMETHODIMP +UploadStumbleRunnable::Run() +{ + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = Upload(); + if (NS_FAILED(rv)) { + WriteStumbleOnThread::UploadEnded(false); + } + return NS_OK; +} + +nsresult +UploadStumbleRunnable::Upload() +{ + nsresult rv; + RefPtr<nsVariant> variant = new nsVariant(); + + rv = variant->SetAsISupports(mUploadInputStream); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIXMLHttpRequest> xhr = do_CreateInstance(NS_XMLHTTPREQUEST_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIScriptSecurityManager> secman = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrincipal> systemPrincipal; + rv = secman->GetSystemPrincipal(getter_AddRefs(systemPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = xhr->Init(systemPrincipal, nullptr, nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURLFormatter> formatter = + do_CreateInstance("@mozilla.org/toolkit/URLFormatterService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsString url; + rv = formatter->FormatURLPref(NS_LITERAL_STRING("geo.stumbler.url"), url); + NS_ENSURE_SUCCESS(rv, rv); + + rv = xhr->Open(NS_LITERAL_CSTRING("POST"), NS_ConvertUTF16toUTF8(url), false, EmptyString(), EmptyString()); + NS_ENSURE_SUCCESS(rv, rv); + + xhr->SetRequestHeader(NS_LITERAL_CSTRING("Content-Type"), NS_LITERAL_CSTRING("gzip")); + xhr->SetMozBackgroundRequest(true); + // 60s timeout + xhr->SetTimeout(60 * 1000); + + nsCOMPtr<EventTarget> target(do_QueryInterface(xhr)); + RefPtr<nsIDOMEventListener> listener = new UploadEventListener(xhr); + + const char* const sEventStrings[] = { + // nsIXMLHttpRequestEventTarget event types + "abort", + "error", + "load", + "timeout" + }; + + for (uint32_t index = 0; index < MOZ_ARRAY_LENGTH(sEventStrings); index++) { + nsAutoString eventType = NS_ConvertASCIItoUTF16(sEventStrings[index]); + rv = target->AddEventListener(eventType, listener, false); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = xhr->Send(variant); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(UploadEventListener, nsIDOMEventListener) + +UploadEventListener::UploadEventListener(nsIXMLHttpRequest* aXHR) +: mXHR(aXHR) +{ +} + +NS_IMETHODIMP +UploadEventListener::HandleEvent(nsIDOMEvent* aEvent) +{ + nsString type; + if (NS_FAILED(aEvent->GetType(type))) { + STUMBLER_ERR("Failed to get event type"); + WriteStumbleOnThread::UploadEnded(false); + return NS_ERROR_FAILURE; + } + + if (type.EqualsLiteral("load")) { + STUMBLER_DBG("Got load Event\n"); + } else if (type.EqualsLiteral("error") && mXHR) { + STUMBLER_ERR("Upload Error"); + } else { + STUMBLER_DBG("Receive %s Event", NS_ConvertUTF16toUTF8(type).get()); + } + + uint32_t statusCode = 0; + bool doDelete = false; + if (!mXHR) { + return NS_OK; + } + nsresult rv = mXHR->GetStatus(&statusCode); + if (NS_SUCCEEDED(rv)) { + STUMBLER_DBG("statuscode %d \n", statusCode); + } + + if (200 == statusCode || 400 == statusCode) { + doDelete = true; + } + + WriteStumbleOnThread::UploadEnded(doDelete); + nsCOMPtr<EventTarget> target(do_QueryInterface(mXHR)); + + const char* const sEventStrings[] = { + // nsIXMLHttpRequestEventTarget event types + "abort", + "error", + "load", + "timeout" + }; + + for (uint32_t index = 0; index < MOZ_ARRAY_LENGTH(sEventStrings); index++) { + nsAutoString eventType = NS_ConvertASCIItoUTF16(sEventStrings[index]); + rv = target->RemoveEventListener(eventType, this, false); + } + + mXHR = nullptr; + return NS_OK; +} diff --git a/dom/system/gonk/mozstumbler/UploadStumbleRunnable.h b/dom/system/gonk/mozstumbler/UploadStumbleRunnable.h new file mode 100644 index 000000000..462665a86 --- /dev/null +++ b/dom/system/gonk/mozstumbler/UploadStumbleRunnable.h @@ -0,0 +1,46 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#ifndef UPLOADSTUMBLERUNNABLE_H +#define UPLOADSTUMBLERUNNABLE_H + +#include "nsIDOMEventListener.h" + +class nsIXMLHttpRequest; +class nsIInputStream; + +/* + This runnable is managed by WriteStumbleOnThread only, see that class + for how this is scheduled. + */ +class UploadStumbleRunnable final : public Runnable +{ +public: + explicit UploadStumbleRunnable(nsIInputStream* aUploadInputStream); + + NS_IMETHOD Run() override; +private: + virtual ~UploadStumbleRunnable() {} + nsCOMPtr<nsIInputStream> mUploadInputStream; + nsresult Upload(); +}; + + +class UploadEventListener : public nsIDOMEventListener +{ +public: + UploadEventListener(nsIXMLHttpRequest* aXHR); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + +protected: + virtual ~UploadEventListener() {} + nsCOMPtr<nsIXMLHttpRequest> mXHR; +}; + +#endif diff --git a/dom/system/gonk/mozstumbler/WriteStumbleOnThread.cpp b/dom/system/gonk/mozstumbler/WriteStumbleOnThread.cpp new file mode 100644 index 000000000..e58e771c4 --- /dev/null +++ b/dom/system/gonk/mozstumbler/WriteStumbleOnThread.cpp @@ -0,0 +1,321 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WriteStumbleOnThread.h" +#include "StumblerLogging.h" +#include "UploadStumbleRunnable.h" +#include "nsDumpUtils.h" +#include "nsGZFileWriter.h" +#include "nsIFileStreams.h" +#include "nsIInputStream.h" +#include "nsPrintfCString.h" + +#define MAXFILESIZE_KB (15 * 1024) +#define ONEDAY_IN_MSEC (24 * 60 * 60 * 1000) +#define MAX_UPLOAD_ATTEMPTS 20 + +mozilla::Atomic<bool> WriteStumbleOnThread::sIsFileWaitingForUpload(false); +mozilla::Atomic<bool> WriteStumbleOnThread::sIsAlreadyRunning(false); +WriteStumbleOnThread::UploadFreqGuard WriteStumbleOnThread::sUploadFreqGuard = {0}; + +#define FILENAME_INPROGRESS NS_LITERAL_CSTRING("stumbles.json.gz") +#define FILENAME_COMPLETED NS_LITERAL_CSTRING("stumbles.done.json.gz") +#define OUTPUT_DIR NS_LITERAL_CSTRING("mozstumbler") + +class DeleteRunnable : public Runnable +{ + public: + DeleteRunnable() {} + + NS_IMETHOD + Run() override + { + nsCOMPtr<nsIFile> tmpFile; + nsresult rv = nsDumpUtils::OpenTempFile(FILENAME_COMPLETED, + getter_AddRefs(tmpFile), + OUTPUT_DIR, + nsDumpUtils::CREATE); + if (NS_SUCCEEDED(rv)) { + tmpFile->Remove(true); + } + // critically, this sets this flag to false so writing can happen again + WriteStumbleOnThread::sIsAlreadyRunning = false; + WriteStumbleOnThread::sIsFileWaitingForUpload = false; + return NS_OK; + } + + private: + ~DeleteRunnable() {} +}; + +bool +WriteStumbleOnThread::IsFileWaitingForUpload() +{ + return sIsFileWaitingForUpload; +} + +void +WriteStumbleOnThread::UploadEnded(bool deleteUploadFile) +{ + if (!deleteUploadFile) { + sIsAlreadyRunning = false; + return; + } + + nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + nsCOMPtr<nsIRunnable> event = new DeleteRunnable(); + target->Dispatch(event, NS_DISPATCH_NORMAL); +} + +void +WriteStumbleOnThread::WriteJSON(Partition aPart) +{ + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIFile> tmpFile; + nsresult rv; + rv = nsDumpUtils::OpenTempFile(FILENAME_INPROGRESS, getter_AddRefs(tmpFile), + OUTPUT_DIR, nsDumpUtils::CREATE); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("Open a file for stumble failed"); + return; + } + + RefPtr<nsGZFileWriter> gzWriter = new nsGZFileWriter(nsGZFileWriter::Append); + rv = gzWriter->Init(tmpFile); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("gzWriter init failed"); + return; + } + + /* + The json format is like below. + {items:[ + {item}, + {item}, + {item} + ]} + */ + + // Need to add "]}" after the last item + if (aPart == Partition::End) { + rv = gzWriter->Write("]}"); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("gzWriter Write failed"); + } + + rv = gzWriter->Finish(); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("ostream finish failed"); + } + + nsCOMPtr<nsIFile> targetFile; + nsresult rv = nsDumpUtils::OpenTempFile(FILENAME_COMPLETED, getter_AddRefs(targetFile), + OUTPUT_DIR, nsDumpUtils::CREATE); + nsAutoString targetFilename; + rv = targetFile->GetLeafName(targetFilename); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("Get Filename failed"); + return; + } + rv = targetFile->Remove(true); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("Remove File failed"); + return; + } + // Rename tmpfile + rv = tmpFile->MoveTo(/* directory */ nullptr, targetFilename); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("Rename File failed"); + return; + } + return; + } + + // Need to add "{items:[" before the first item + if (aPart == Partition::Begining) { + rv = gzWriter->Write("{\"items\":[{"); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("ostream write begining failed"); + } + } else if (aPart == Partition::Middle) { + rv = gzWriter->Write(",{"); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("ostream write middle failed"); + } + } + rv = gzWriter->Write(mDesc.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("ostream write mDesc failed"); + } + // one item is ended with '}' (e.g. {item}) + rv = gzWriter->Write("}"); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("ostream write end failed"); + } + + rv = gzWriter->Finish(); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("ostream finish failed"); + } + + // check if it is the end of this file + int64_t fileSize = 0; + rv = tmpFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("GetFileSize failed"); + return; + } + if (fileSize >= MAXFILESIZE_KB) { + WriteJSON(Partition::End); + return; + } +} + +WriteStumbleOnThread::Partition +WriteStumbleOnThread::GetWritePosition() +{ + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIFile> tmpFile; + nsresult rv = nsDumpUtils::OpenTempFile(FILENAME_INPROGRESS, getter_AddRefs(tmpFile), + OUTPUT_DIR, nsDumpUtils::CREATE); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("Open a file for stumble failed"); + return Partition::Unknown; + } + + int64_t fileSize = 0; + rv = tmpFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("GetFileSize failed"); + return Partition::Unknown; + } + + if (fileSize == 0) { + return Partition::Begining; + } else if (fileSize >= MAXFILESIZE_KB) { + return Partition::End; + } else { + return Partition::Middle; + } +} + +NS_IMETHODIMP +WriteStumbleOnThread::Run() +{ + MOZ_ASSERT(!NS_IsMainThread()); + + bool b = sIsAlreadyRunning.exchange(true); + if (b) { + return NS_OK; + } + + UploadFileStatus status = GetUploadFileStatus(); + + if (UploadFileStatus::NoFile != status) { + if (UploadFileStatus::ExistsAndReadyToUpload == status) { + sIsFileWaitingForUpload = true; + Upload(); + return NS_OK; + } + } else { + Partition partition = GetWritePosition(); + if (partition == Partition::Unknown) { + STUMBLER_ERR("GetWritePosition failed, skip once"); + } else { + WriteJSON(partition); + } + } + + sIsFileWaitingForUpload = false; + sIsAlreadyRunning = false; + return NS_OK; +} + + +/* + If the upload file exists, then check if it is one day old. + • if it is a day old -> ExistsAndReadyToUpload + • if it is less than the current day old -> Exists + • otherwise -> NoFile + + The Exists case means that the upload and the stumbling is rate limited + per-day to the size of the one file. + */ +WriteStumbleOnThread::UploadFileStatus +WriteStumbleOnThread::GetUploadFileStatus() +{ + nsCOMPtr<nsIFile> tmpFile; + nsresult rv = nsDumpUtils::OpenTempFile(FILENAME_COMPLETED, getter_AddRefs(tmpFile), + OUTPUT_DIR, nsDumpUtils::CREATE); + int64_t fileSize; + rv = tmpFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("GetFileSize failed"); + return UploadFileStatus::NoFile; + } + if (fileSize <= 0) { + tmpFile->Remove(true); + return UploadFileStatus::NoFile; + } + + PRTime lastModifiedTime; + tmpFile->GetLastModifiedTime(&lastModifiedTime); + if ((PR_Now() / PR_USEC_PER_MSEC) - lastModifiedTime >= ONEDAY_IN_MSEC) { + return UploadFileStatus::ExistsAndReadyToUpload; + } + return UploadFileStatus::Exists; +} + +void +WriteStumbleOnThread::Upload() +{ + MOZ_ASSERT(!NS_IsMainThread()); + + time_t seconds = time(0); + int day = seconds / (60 * 60 * 24); + + if (sUploadFreqGuard.daySinceEpoch < day) { + sUploadFreqGuard.daySinceEpoch = day; + sUploadFreqGuard.attempts = 0; + } + + sUploadFreqGuard.attempts++; + if (sUploadFreqGuard.attempts > MAX_UPLOAD_ATTEMPTS) { + STUMBLER_ERR("Too many upload attempts today"); + sIsAlreadyRunning = false; + return; + } + + nsCOMPtr<nsIFile> tmpFile; + nsresult rv = nsDumpUtils::OpenTempFile(FILENAME_COMPLETED, getter_AddRefs(tmpFile), + OUTPUT_DIR, nsDumpUtils::CREATE); + int64_t fileSize; + rv = tmpFile->GetFileSize(&fileSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + STUMBLER_ERR("GetFileSize failed"); + sIsAlreadyRunning = false; + return; + } + + if (fileSize <= 0) { + sIsAlreadyRunning = false; + return; + } + + // prepare json into nsIInputStream + nsCOMPtr<nsIInputStream> inStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream), tmpFile); + if (NS_FAILED(rv)) { + sIsAlreadyRunning = false; + return; + } + + RefPtr<nsIRunnable> uploader = new UploadStumbleRunnable(inStream); + NS_DispatchToMainThread(uploader); +} diff --git a/dom/system/gonk/mozstumbler/WriteStumbleOnThread.h b/dom/system/gonk/mozstumbler/WriteStumbleOnThread.h new file mode 100644 index 000000000..104cf9bdd --- /dev/null +++ b/dom/system/gonk/mozstumbler/WriteStumbleOnThread.h @@ -0,0 +1,91 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WriteStumbleOnThread_H +#define WriteStumbleOnThread_H + +#include "mozilla/Atomics.h" + +class DeleteRunnable; + +/* + This class is the entry point to stumbling, in that it + receives the location+cell+wifi string and writes it + to disk, or instead, it calls UploadStumbleRunnable + to upload the data. + + Writes will happen until the file is a max size, then stop. + Uploads will happen only when the file is one day old. + The purpose of these decisions is to have very simple rate-limiting + on the writes, as well as the uploads. + + There is only one file active; it is either being used for writing, + or for uploading. If the file is ready for uploading, no further + writes will take place until this file has been uploaded. + This can mean writing might not take place for days until the uploaded + file is processed. This is correct by-design. + + A notable limitation is that the upload is triggered by a location event, + this is used as an arbitrary and simple trigger. In future, there are + better events that can be used, such as detecting network activity. + + This thread is guarded so that only one instance is active (see the + mozilla::Atomics used for this). + */ +class WriteStumbleOnThread : public mozilla::Runnable +{ +public: + explicit WriteStumbleOnThread(const nsCString& aDesc) + : mDesc(aDesc) + {} + + NS_IMETHOD Run() override; + + static void UploadEnded(bool deleteUploadFile); + + // Used externally to determine if cell+wifi scans should happen + // (returns false for that case). + static bool IsFileWaitingForUpload(); + +private: + friend class DeleteRunnable; + + enum class Partition { + Begining, + Middle, + End, + Unknown + }; + + enum class UploadFileStatus { + NoFile, Exists, ExistsAndReadyToUpload + }; + + ~WriteStumbleOnThread() {} + + Partition GetWritePosition(); + UploadFileStatus GetUploadFileStatus(); + void WriteJSON(Partition aPart); + void Upload(); + + nsCString mDesc; + + // Only run one instance of this + static mozilla::Atomic<bool> sIsAlreadyRunning; + + static mozilla::Atomic<bool> sIsFileWaitingForUpload; + + // Limit the upload attempts per day. If the device is rebooted + // this resets the allowed attempts, which is acceptable. + struct UploadFreqGuard { + int attempts; + int daySinceEpoch; + }; + static UploadFreqGuard sUploadFreqGuard; + +}; + +#endif diff --git a/dom/system/gonk/nsIAudioManager.idl b/dom/system/gonk/nsIAudioManager.idl new file mode 100644 index 000000000..c2eb62b21 --- /dev/null +++ b/dom/system/gonk/nsIAudioManager.idl @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, builtinclass, uuid(df31c280-1ef1-11e5-867f-0800200c9a66)] +interface nsIAudioManager : nsISupports +{ + /** + * Microphone muted? + */ + attribute boolean microphoneMuted; + + /** + * Set the phone's audio mode. + */ + const long PHONE_STATE_INVALID = -2; + const long PHONE_STATE_CURRENT = -1; + const long PHONE_STATE_NORMAL = 0; + const long PHONE_STATE_RINGTONE = 1; + const long PHONE_STATE_IN_CALL = 2; + const long PHONE_STATE_IN_COMMUNICATION = 3; + + attribute long phoneState; + + /** + * Configure a particular device ("force") to be used for one of the uses + * (communication, media playback, etc.) + */ + const long FORCE_NONE = 0; // the default + const long FORCE_SPEAKER = 1; + const long FORCE_HEADPHONES = 2; + const long FORCE_BT_SCO = 3; + const long FORCE_BT_A2DP = 4; + const long FORCE_WIRED_ACCESSORY = 5; + const long FORCE_BT_CAR_DOCK = 6; + const long FORCE_BT_DESK_DOCK = 7; + const long FORCE_ANALOG_DOCK = 8; + const long FORCE_DIGITAL_DOCK = 9; + const long FORCE_NO_BT_A2DP = 10; + const long USE_COMMUNICATION = 0; + const long USE_MEDIA = 1; + const long USE_RECORD = 2; + const long USE_DOCK = 3; + + void setForceForUse(in long usage, in long force); + long getForceForUse(in long usage); + + /** + * These functions would be used when we enable the new volume control API + * (mozAudioChannelManager). The range of volume index is from 0 to N. + * More details on : https://gist.github.com/evanxd/41d8e2d91c5201a42bfa + */ + void setAudioChannelVolume(in unsigned long channel, in unsigned long index); + unsigned long getAudioChannelVolume(in unsigned long channel); + unsigned long getMaxAudioChannelVolume(in unsigned long channel); +}; diff --git a/dom/system/gonk/nsIDataCallInterfaceService.idl b/dom/system/gonk/nsIDataCallInterfaceService.idl new file mode 100644 index 000000000..c387879fa --- /dev/null +++ b/dom/system/gonk/nsIDataCallInterfaceService.idl @@ -0,0 +1,268 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(6b66446a-7000-438f-8e1b-b56b4cbf4fa9)] +interface nsIDataCall : nsISupports +{ + /** + * Data call fail cause. One of the nsIDataCallInterface.DATACALL_FAIL_* + * values. + */ + readonly attribute long failCause; + + /** + * If failCause != nsIDataCallInterface.DATACALL_FAIL_NONE, this field + * indicates the suggested retry back-off timer. The unit is milliseconds. + */ + readonly attribute long suggestedRetryTime; + + /** + * Context ID, uniquely identifies this call. + */ + readonly attribute long cid; + + /** + * Data call network state. One of the nsIDataCallInterface.DATACALL_STATE_* + * values. + */ + readonly attribute long active; + + /** + * Data call connection type. One of the + * nsIDataCallInterface.DATACALL_PDP_TYPE_* values. + */ + readonly attribute long pdpType; + + /** + * The network interface name. + */ + readonly attribute DOMString ifname; + + /** + * A space-delimited list of addresses with optional "/" prefix length. + */ + readonly attribute DOMString addresses; + + /** + * A space-delimited list of DNS server addresses. + */ + readonly attribute DOMString dnses; + + /** + * A space-delimited list of default gateway addresses. + */ + readonly attribute DOMString gateways; + + /** + * A space-delimited list of Proxy Call State Control Function addresses for + * IMS client. + */ + readonly attribute DOMString pcscf; + + /** + * MTU received from network, -1 if not set or invalid. + */ + readonly attribute long mtu; +}; + +[scriptable, uuid(e119c54b-9354-4ad6-a1ee-18608bde9320)] +interface nsIDataCallInterfaceListener : nsISupports +{ + /** + * Notify data call interface listeners about unsolicited data call state + * changes. + */ + void notifyDataCallListChanged(in uint32_t count, + [array, size_is(count)] in nsIDataCall + dataCalls); +}; + +[scriptable, uuid(db0b640a-3b3a-4f48-84dc-256e176876c2)] +interface nsIDataCallCallback : nsISupports +{ + /** + * Called when setupDataCall() returns succesfully. + */ + void notifySetupDataCallSuccess(in nsIDataCall dataCall); + + /** + * Called when getDataCallList() returns succesfully. + */ + void notifyGetDataCallListSuccess(in uint32_t count, + [array, size_is(count)] in nsIDataCall + dataCalls); + /** + * Called when request returns succesfully. + */ + void notifySuccess(); + + /** + * Called when request returns error. + */ + void notifyError(in AString errorMsg); +}; + +[scriptable, uuid(ec219021-8623-4b9f-aba5-4db58c60684f)] +interface nsIDataCallInterface : nsISupports +{ + /** + * Data fail causes, defined in TS 24.008. + */ + const long DATACALL_FAIL_NONE = 0; + const long DATACALL_FAIL_OPERATOR_BARRED = 0x08; + const long DATACALL_FAIL_INSUFFICIENT_RESOURCES = 0x1A; + const long DATACALL_FAIL_MISSING_UKNOWN_APN = 0x1B; + const long DATACALL_FAIL_UNKNOWN_PDP_ADDRESS_TYPE = 0x1C; + const long DATACALL_FAIL_USER_AUTHENTICATION = 0x1D; + const long DATACALL_FAIL_ACTIVATION_REJECT_GGSN = 0x1E; + const long DATACALL_FAIL_ACTIVATION_REJECT_UNSPECIFIED = 0x1F; + const long DATACALL_FAIL_SERVICE_OPTION_NOT_SUPPORTED = 0x20; + const long DATACALL_FAIL_SERVICE_OPTION_NOT_SUBSCRIBED = 0x21; + const long DATACALL_FAIL_SERVICE_OPTION_OUT_OF_ORDER = 0x22; + const long DATACALL_FAIL_NSAPI_IN_USE = 0x23; + const long DATACALL_FAIL_ONLY_IPV4_ALLOWED = 0x32; + const long DATACALL_FAIL_ONLY_IPV6_ALLOWED = 0x33; + const long DATACALL_FAIL_ONLY_SINGLE_BEARER_ALLOWED = 0x34; + const long DATACALL_FAIL_PROTOCOL_ERRORS = 0x6F; + /* Not mentioned in the specification */ + const long DATACALL_FAIL_VOICE_REGISTRATION_FAIL = -1; + const long DATACALL_FAIL_DATA_REGISTRATION_FAIL = -2; + const long DATACALL_FAIL_SIGNAL_LOST = -3; + const long DATACALL_FAIL_PREF_RADIO_TECH_CHANGED = -4; + const long DATACALL_FAIL_RADIO_POWER_OFF = -5; + const long DATACALL_FAIL_TETHERED_CALL_ACTIVE = -6; + const long DATACALL_FAIL_ERROR_UNSPECIFIED = 0xFFFF; + + /** + * Data call network state. + */ + const long DATACALL_STATE_INACTIVE = 0; + const long DATACALL_STATE_ACTIVE_DOWN = 1; + const long DATACALL_STATE_ACTIVE_UP = 2; + + /** + * Data call authentication type. Must match the values in ril_consts + * RIL_DATACALL_AUTH_TO_GECKO array. + */ + const long DATACALL_AUTH_NONE = 0; + const long DATACALL_AUTH_PAP = 1; + const long DATACALL_AUTH_CHAP = 2; + const long DATACALL_AUTH_PAP_OR_CHAP = 3; + + /** + * Data call protocol type. Must match the values in ril_consts + * RIL_DATACALL_PDP_TYPES array. + */ + const long DATACALL_PDP_TYPE_IPV4 = 0; + const long DATACALL_PDP_TYPE_IPV4V6 = 1; + const long DATACALL_PDP_TYPE_IPV6 = 2; + + /** + * Reason for deactivating data call. + */ + const long DATACALL_DEACTIVATE_NO_REASON = 0; + const long DATACALL_DEACTIVATE_RADIO_SHUTDOWN = 1; + + /** + * Setup data call. + * + * @param apn + * Apn to connect to. + * @param username + * Username for apn. + * @param password + * Password for apn. + * @param authType + * Authentication type. One of the DATACALL_AUTH_* values. + * @param pdpType + * Connection type. One of the DATACALL_PDP_TYPE_* values. + * @param nsIDataCallCallback + * Called when request is finished. + * + * If successful, the notifySetupDataCallSuccess() will be called with the + * new nsIDataCall. + * + * Otherwise, the notifyError() will be called, and the error will be either + * 'RadioNotAvailable', 'OpNotAllowedBeforeRegToNw', + * 'OpNotAllowedDuringVoiceCall', 'RequestNotSupported' or 'GenericFailure'. + */ + void setupDataCall(in AString apn, in AString username, + in AString password, in long authType, + in long pdpType, + in nsIDataCallCallback callback); + + /** + * Deactivate data call. + * + * @param cid + * Context id. + * @param reason + * Disconnect Reason. One of the DATACALL_DEACTIVATE_* values. + * @param nsIDataCallCallback + * Called when request is finished. + * + * If successful, the notifySuccess() will be called. + * + * Otherwise, the notifyError() will be called, and the error will be either + * 'RadioNotAvailable' or 'GenericFailure'. + */ + void deactivateDataCall(in long cid, + in long reason, + in nsIDataCallCallback callback); + + /** + * Get current data call list. + * + * @param nsIDataCallCallback + * Called when request is finished. + * + * If successful, the notifyGetDataCallListSuccess() will be called with the + * list of nsIDataCall(s). + * + * Otherwise, the notifyError() will be called, and the error will be either + * 'RadioNotAvailable' or 'GenericFailure'. + */ + void getDataCallList(in nsIDataCallCallback callback); + + /** + * Set data registration state. + * + * @param attach + * whether to attach data registration or not. + * @param nsIDataCallCallback + * Called when request is finished. + * + * If successful, the notifySuccess() will be called. + * + * Otherwise, the notifyError() will be called, and the error will be either + * 'RadioNotAvailable', 'SubscriptionNotAvailable' or 'GenericFailure'. + */ + void setDataRegistration(in boolean attach, + in nsIDataCallCallback callback); + + /** + * Register to receive unsolicited events from this nsIDataCallInterface. + */ + void registerListener(in nsIDataCallInterfaceListener listener); + + /** + * Unregister to stop receiving unsolicited events from this + * nsIDataCallInterface. + */ + void unregisterListener(in nsIDataCallInterfaceListener listener); +}; + +[scriptable, uuid(64700406-7429-4743-a6ae-f82e9877fd0d)] +interface nsIDataCallInterfaceService : nsISupports +{ + /** + * Get the corresponding data call interface. + * + * @param clientId + * clientId of the data call interface to get. + */ + nsIDataCallInterface getDataCallInterface(in long clientId); +};
\ No newline at end of file diff --git a/dom/system/gonk/nsIDataCallManager.idl b/dom/system/gonk/nsIDataCallManager.idl new file mode 100644 index 000000000..de8477801 --- /dev/null +++ b/dom/system/gonk/nsIDataCallManager.idl @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsINetworkInterface.idl" + +[scriptable, uuid(b8bcd6aa-5b06-4362-a68c-317878429e51)] +interface nsIRilNetworkInfo : nsINetworkInfo +{ + readonly attribute unsigned long serviceId; + readonly attribute DOMString iccId; + + /* The following attributes are for MMS proxy settings. */ + readonly attribute DOMString mmsc; // Empty string if not set. + readonly attribute DOMString mmsProxy; // Empty string if not set. + readonly attribute long mmsPort; // -1 if not set. + + /** + * Get the list of pcscf addresses, could be IPv4 or IPv6. + * + * @param count + * The length of the list of pcscf addresses. + * + * @returns the list of pcscf addresses. + */ + void getPcscf([optional] out unsigned long count, + [array, size_is(count), retval] out wstring pcscf); +}; + +[scriptable, function, uuid(cb2f0f5b-67f4-4c14-93e8-01e66b630464)] +interface nsIDeactivateDataCallsCallback : nsISupports +{ + /** + * Callback function used to notify when all data calls are disconnected. + */ + void notifyDataCallsDisconnected(); +}; + +[scriptable, uuid(e3feec20-36b4-47de-a7a5-e32a65f20186)] +interface nsIDataCallHandler : nsISupports +{ + /** + * PDP APIs + * + * @param networkType + * Mobile network type, that is, + * nsINetworkInterface.NETWORK_TYPE_MOBILE or one of the + * nsINetworkInterface.NETWORK_TYPE_MOBILE_* values. + */ + void setupDataCallByType(in long networkType); + void deactivateDataCallByType(in long networkType); + long getDataCallStateByType(in long networkType); + + /** + * Deactivate all data calls. + * + * @param callback + * Callback to notify when all data calls are disconnected. + */ + void deactivateDataCalls(in nsIDeactivateDataCallsCallback callback); + + /** + * Called to reconsider data call state. + */ + void updateRILNetworkInterface(); +}; + +[scriptable, uuid(2c46e37d-88dc-4d25-bb37-e1c0d3e9cb5f)] +interface nsIDataCallManager : nsISupports +{ + readonly attribute long dataDefaultServiceId; + + /** + * Get the corresponding data call handler. + * + * @param clientId + * clientId of the data call handler to get. + */ + nsIDataCallHandler getDataCallHandler(in unsigned long clientId); +};
\ No newline at end of file diff --git a/dom/system/gonk/nsIGonkDataCallInterfaceService.idl b/dom/system/gonk/nsIGonkDataCallInterfaceService.idl new file mode 100644 index 000000000..240ca6bab --- /dev/null +++ b/dom/system/gonk/nsIGonkDataCallInterfaceService.idl @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIDataCallInterfaceService.idl" + +[scriptable, uuid(f008d00c-e2b8-49b2-8f88-19111577938e)] +interface nsIGonkDataCallInterfaceService : nsIDataCallInterfaceService +{ + /** + * Called by RadioInterface or lower layer to notify about data call list + * changes. + */ + void notifyDataCallListChanged(in unsigned long clientId, + in uint32_t count, + [array, size_is(count)] in nsIDataCall + dataCalls); +};
\ No newline at end of file diff --git a/dom/system/gonk/nsINetworkInterface.idl b/dom/system/gonk/nsINetworkInterface.idl new file mode 100644 index 000000000..bd40e751a --- /dev/null +++ b/dom/system/gonk/nsINetworkInterface.idl @@ -0,0 +1,108 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(4816a559-5620-4cb5-8433-ff0b25e6622f)] +interface nsINetworkInfo : nsISupports +{ + const long NETWORK_STATE_UNKNOWN = -1; + const long NETWORK_STATE_CONNECTING = 0; + const long NETWORK_STATE_CONNECTED = 1; + const long NETWORK_STATE_DISCONNECTING = 2; + const long NETWORK_STATE_DISCONNECTED = 3; + const long NETWORK_STATE_ENABLED = 4; + const long NETWORK_STATE_DISABLED = 5; + + /** + * Current network state, one of the NETWORK_STATE_* constants. + * + * When this changes, network interface implementations notify with + * updateNetworkInterface() API. + */ + readonly attribute long state; + + const long NETWORK_TYPE_UNKNOWN = -1; + const long NETWORK_TYPE_WIFI = 0; + const long NETWORK_TYPE_MOBILE = 1; + const long NETWORK_TYPE_MOBILE_MMS = 2; + const long NETWORK_TYPE_MOBILE_SUPL = 3; + const long NETWORK_TYPE_WIFI_P2P = 4; + const long NETWORK_TYPE_MOBILE_IMS = 5; + const long NETWORK_TYPE_MOBILE_DUN = 6; + const long NETWORK_TYPE_MOBILE_FOTA = 7; + const long NETWORK_TYPE_ETHERNET = 8; + + /** + * Network type. One of the NETWORK_TYPE_* constants. + */ + readonly attribute long type; + + /** + * Interface name of the network interface this network info belongs to. + */ + readonly attribute DOMString name; + + /** + * Get the list of ip addresses and prefix lengths, ip address could be IPv4 + * or IPv6, typically 1 IPv4 or 1 IPv6 or one of each. + * + * @param ips + * The list of ip addresses retrieved. + * @param prefixLengths + * The list of prefix lengths retrieved. + * + * @returns the length of the lists. + */ + void getAddresses([array, size_is(count)] out wstring ips, + [array, size_is(count)] out unsigned long prefixLengths, + [retval] out unsigned long count); + + /** + * Get the list of gateways, could be IPv4 or IPv6, typically 1 IPv4 or 1 + * IPv6 or one of each. + * + * @param count + * The length of the list of gateways + * + * @returns the list of gateways. + */ + void getGateways([optional] out unsigned long count, + [array, size_is(count), retval] out wstring gateways); + + /** + * Get the list of dnses, could be IPv4 or IPv6. + * + * @param count + * The length of the list of dnses. + * + * @returns the list of dnses. + */ + void getDnses([optional] out unsigned long count, + [array, size_is(count), retval] out wstring dnses); +}; + +[scriptable, uuid(8b1345fa-b34c-41b3-8d21-09f961bf8887)] +interface nsINetworkInterface : nsISupports +{ + /** + * The network information about this network interface. + */ + readonly attribute nsINetworkInfo info; + + /** + * The host name of the http proxy server. + */ + readonly attribute DOMString httpProxyHost; + + /* + * The port number of the http proxy server. + */ + readonly attribute long httpProxyPort; + + /* + * The Maximun Transmit Unit for this network interface, -1 if not set. + */ + readonly attribute long mtu; +}; diff --git a/dom/system/gonk/nsINetworkInterfaceListService.idl b/dom/system/gonk/nsINetworkInterfaceListService.idl new file mode 100644 index 000000000..0c224842e --- /dev/null +++ b/dom/system/gonk/nsINetworkInterfaceListService.idl @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsINetworkInfo; + +[scriptable, uuid(55779d32-1e28-4f43-af87-09d04bc3cce9)] +interface nsINetworkInterfaceList : nsISupports +{ + /** + * Number of the network interfaces that is available. + */ + long getNumberOfInterface(); + + /** + * Get the i-th interface info info from the list. + * @param interfaceIndex index of interface, from 0 to number of interface - 1. + */ + nsINetworkInfo getInterfaceInfo(in long interfaceIndex); +}; + +[scriptable, uuid(21d7fc8b-28c4-4a4f-a15e-1f9defbc2cec)] +interface nsINetworkInterfaceListService : nsISupports +{ + const long LIST_NOT_INCLUDE_MMS_INTERFACES = (1 << 0); + const long LIST_NOT_INCLUDE_SUPL_INTERFACES = (1 << 1); + const long LIST_NOT_INCLUDE_IMS_INTERFACES = (1 << 2); + const long LIST_NOT_INCLUDE_DUN_INTERFACES = (1 << 3); + const long LIST_NOT_INCLUDE_FOTA_INTERFACES = (1 << 4); + + /** + * Obtain a list of network interfaces that satisfy the specified condition. + * @param condition flags that specify the interfaces to be returned. This + * can be OR combination of LIST_* flags, or zero to make all available + * interfaces returned. + */ + nsINetworkInterfaceList getDataInterfaceList(in long condition); +}; diff --git a/dom/system/gonk/nsINetworkManager.idl b/dom/system/gonk/nsINetworkManager.idl new file mode 100644 index 000000000..0da123796 --- /dev/null +++ b/dom/system/gonk/nsINetworkManager.idl @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsINetworkInfo; +interface nsINetworkInterface; + +/** + * Manage network interfaces. + */ +[scriptable, uuid(1ba9346b-53b5-4660-9dc6-58f0b258d0a6)] +interface nsINetworkManager : nsISupports +{ + /** + * Register the given network interface with the network manager. + * + * Consumers will be notified with the 'network-interface-registered' + * observer notification. + * + * Throws if there's already an interface registered with the same network id. + * + * @param network + * Network interface to register. + */ + void registerNetworkInterface(in nsINetworkInterface network); + + /** + * Update the routes and DNSes according the state of the given network. + * + * Consumers will be notified with the 'network-connection-state-changed' + * observer notification. + * + * Throws an exception if the specified network interface object isn't + * registered. + * + * @param network + * Network interface to update. + */ + void updateNetworkInterface(in nsINetworkInterface network); + + /** + * Unregister the given network interface from the network manager. + * + * Consumers will be notified with the 'network-interface-unregistered' + * observer notification. + * + * Throws an exception if the specified network interface object isn't + * registered. + * + * @param network + * Network interface to unregister. + */ + void unregisterNetworkInterface(in nsINetworkInterface network); + + /** + * Object containing all known network information, keyed by their + * network id. Network id is composed of a sub-id + '-' + network + * type. For mobile network types, sub-id is 'ril' + service id; for + * non-mobile network types, sub-id is always 'device'. + */ + readonly attribute jsval allNetworkInfo; + + /** + * Priority list of network types. An array of + * nsINetworkInterface::NETWORK_TYPE_* constants. + * + * The piror position of the type indicates the higher priority. The priority + * is used to determine route when there are multiple connected networks. + */ + attribute jsval networkTypePriorityList; + + /** + * The preferred network type. One of the + * nsINetworkInterface::NETWORK_TYPE_* constants. + * + * This attribute is used for setting default route to favor + * interfaces with given type. This can be overriden by calling + * overrideDefaultRoute(). + */ + attribute long preferredNetworkType; + + /** + * The network information of the network interface handling all network + * traffic. + * + * When this changes, the 'network-active-changed' observer + * notification is dispatched. + */ + readonly attribute nsINetworkInfo activeNetworkInfo; + + /** + * Override the default behaviour for preferredNetworkType and route + * all network traffic through the the specified interface. + * + * Consumers can observe changes to the active network by subscribing to + * the 'network-active-changed' observer notification. + * + * @param network + * Network to route all network traffic to. If this is null, + * a previous override is canceled. + */ + long overrideActive(in nsINetworkInterface network); + + /** + * Add host route to the specified network into routing table. + * + * @param network + * The network information for the host to be routed to. + * @param host + * The host to be added. + * The host will be resolved in advance if it's not an ip-address. + * + * @return a Promise + * resolved if added; rejected, otherwise. + */ + jsval addHostRoute(in nsINetworkInfo network, + in DOMString host); + + /** + * Remove host route to the specified network from routing table. + * + * @param network + * The network information for the routing to be removed from. + * @param host + * The host routed to the network. + * The host will be resolved in advance if it's not an ip-address. + * + * @return a Promise + * resolved if removed; rejected, otherwise. + */ + jsval removeHostRoute(in nsINetworkInfo network, + in DOMString host); +}; diff --git a/dom/system/gonk/nsINetworkService.idl b/dom/system/gonk/nsINetworkService.idl new file mode 100644 index 000000000..50a468494 --- /dev/null +++ b/dom/system/gonk/nsINetworkService.idl @@ -0,0 +1,619 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, function, uuid(91824160-fb25-11e1-a21f-0800200c9a66)] +interface nsIWifiTetheringCallback : nsISupports +{ + /** + * Callback function used to report status to WifiManager. + * + * @param error + * An error message if the operation wasn't successful, + * or `null` if it was. + */ + void wifiTetheringEnabledChange(in jsval error); +}; + +[scriptable, function, uuid(9c128e68-5e4b-4626-bb88-84ec54cce5d8)] +interface nsINetworkStatsCallback : nsISupports +{ + void networkStatsAvailable(in boolean success, + in unsigned long rxBytes, + in unsigned long txBytes, + in unsigned long long timestamp); +}; + +[scriptable, function, uuid(0706bfa2-ac2d-11e2-9a8d-7b6d988d4767)] +interface nsINetworkUsageAlarmCallback : nsISupports +{ + void networkUsageAlarmResult(in jsval error); +}; + +[scriptable, function, uuid(9ede8720-f8bc-11e2-b778-0800200c9a66)] +interface nsIWifiOperationModeCallback : nsISupports +{ + /** + * Callback function used to report result to WifiManager. + * + * @param error + * An error message if the operation wasn't successful, + * or `null` if it was. + */ + void wifiOperationModeResult(in jsval error); +}; + +[scriptable, function, uuid(097878b0-19fc-11e3-8ffd-0800200c9a66)] +interface nsISetDhcpServerCallback : nsISupports +{ + /** + * Callback function used to report the DHCP server set result + * + * @param error + * An error message if the operation wasn't successful, + * or `null` if it was. + */ + void dhcpServerResult(in jsval error); +}; + +[scriptable, function, uuid(32407c50-46c7-11e3-8f96-0800200c9a66)] +interface nsIUsbTetheringCallback : nsISupports +{ + /** + * Callback function used to report status of enabling usb tethering. + * + * @param error + * An error message if the operation wasn't successful, + * or `null` if it was. + */ + void usbTetheringEnabledChange(in jsval error); +}; + +[scriptable, function, uuid(055fd560-46ad-11e3-8f96-0800200c9a66)] +interface nsIEnableUsbRndisCallback : nsISupports +{ + /** + * Callback function used to report the status of enabling/disabling usb rndis. + * + * @param success + * Boolean to indicate the operation is successful or not. + * @param enable + * Boolean to indicate if we are enabling or disabling usb rndis. + */ + void enableUsbRndisResult(in boolean success, in boolean enable); +}; + +[scriptable, function, uuid(4f08cc30-46ad-11e3-8f96-0800200c9a66)] +interface nsIUpdateUpStreamCallback : nsISupports +{ + /** + * Callback function used to report the result of updating upstream. + * + * @param success + * Boolean to indicate the operation is successful or not. + * @param externalIfname + * The external interface name. + */ + void updateUpStreamResult(in boolean success, in DOMString externalIfname); +}; + +[scriptable, function, uuid(eedca6c0-1310-11e4-9191-0800200c9a66)] +interface nsISetDnsCallback : nsISupports +{ + /** + * Callback function used to report the result of setting DNS server. + * + * @param error + * An error message if the operation wasn't successful, + * or `null` if it was. + */ + void setDnsResult(in jsval error); +}; + +[scriptable, function, uuid(5d0e1a60-1187-11e4-9191-0800200c9a66)] +interface nsINativeCommandCallback : nsISupports +{ + /** + * Callback function used to report the result of a network native command. + * + * @param success + * Boolean to indicate the operation is successful or not. + */ + void nativeCommandResult(in boolean success); +}; + +[scriptable, function, uuid(694abb80-1187-11e4-9191-0800200c9a66)] +interface nsIDhcpRequestCallback : nsISupports +{ + /** + * Callback function used to report the result of DHCP client request. + * + * @param success + * Boolean to indicate the operation is successful or not. + * + * @param dhcpInfo + * An object to represent the successful DHCP request: + * + * - gateway_str: string + * - dns1_str: string + * - dns2_str: string + * - mask_str: string + * - server_str: string + * - vendor_str: string + * - lease: long + * - mask: long + * - ipaddr: long + * - gateway: long + * - dns1: long + * - dns2: long + * - server: long + */ + void dhcpRequestResult(in boolean success, in jsval dhcpInfo); +}; + +[scriptable, function, uuid(88e3ee22-f1b3-4fa0-8a5d-793fb827c42c)] +interface nsIGetInterfacesCallback : nsISupports +{ + /** + * Callback function used to return the list of existing network interfaces. + * + * @param success + * Boolean to indicate the operation is successful or not. + * @param interfaceList + * An array of interface name. + */ + void getInterfacesResult(in boolean success, in jsval interfaceList); +}; + +[scriptable, function, uuid(064e02a3-d2c0-42c5-a293-1efa84056100)] +interface nsIGetInterfaceConfigCallback : nsISupports +{ + /** + * Callback function used to return the network config of a given interface. + * + * @param success + * Boolean to indicate the operation is successful or not. + * @param result + * .ip: Ip address. + * .prefix: mask length. + * .link: network link properties. + * .mac: mac address. + */ + void getInterfaceConfigResult(in boolean success, in jsval result); +}; + +[scriptable, function, uuid(b370f360-6ba8-4517-a4f9-31e8f004ee91)] +interface nsISetInterfaceConfigCallback : nsISupports +{ + /** + * Callback function used to set network config for a specified interface. + * + * @param success + * Boolean to indicate the operation is successful or not. + */ + void setInterfaceConfigResult(in boolean success); +}; + +/** + * Provide network services. + */ +[scriptable, uuid(e16fe98f-9f63-48fe-82ba-8d1a1b7c6a57)] +interface nsINetworkService : nsISupports +{ + const long MODIFY_ROUTE_ADD = 0; + const long MODIFY_ROUTE_REMOVE = 1; + + /** + * Enable or disable Wifi Tethering + * + * @param enabled + * Boolean that indicates whether tethering should be enabled (true) or disabled (false). + * @param config + * The Wifi Tethering configuration from settings db. + * @param callback + * Callback function used to report status to WifiManager. + */ + void setWifiTethering(in boolean enabled, + in jsval config, + in nsIWifiTetheringCallback callback); + + /** + * Enable or disable DHCP server + * + * @param enabled + * Boolean that indicates enabling or disabling DHCP server. + * + * @param config + * Config used to enable the DHCP server. It contains + * .startIp start of the ip lease range (string) + * .endIp end of the ip lease range (string) + * .serverIp ip of the DHCP server (string) + * .maskLength the length of the subnet mask + * .ifname the interface name + * + * As for disabling the DHCP server, put this value |null|. + * + * @param callback + * Callback function used to report status. + */ + void setDhcpServer(in boolean enabled, + in jsval config, + in nsISetDhcpServerCallback callback); + + + /** + * Retrieve network interface stats. + * + * @param networkName + * Select the Network interface to request estats. + * + * @param callback + * Callback to notify result and provide stats, connectionType + * and the date when stats are retrieved + */ + void getNetworkInterfaceStats(in DOMString networkName, in nsINetworkStatsCallback callback); + + /** + * Set Alarm of usage per interface + * + * @param networkName + * Select the Network interface to set an alarm. + * + * @param threshold + * Amount of data that will trigger the alarm. + * + * @param callback + * Callback to notify the result. + * + * @return false if there is no interface registered for the networkType param. + */ + boolean setNetworkInterfaceAlarm(in DOMString networkName, + in long long threshold, + in nsINetworkUsageAlarmCallback callback); + + /** + * Reload Wifi firmware to specific operation mode. + * + * @param interfaceName + * Wifi Network interface name. + * + * @param mode + * AP - Access pointer mode. + * P2P - Peer to peer connection mode. + * STA - Station mode. + * + * @param callback + * Callback to notify Wifi firmware reload result. + */ + void setWifiOperationMode(in DOMString interfaceName, + in DOMString mode, + in nsIWifiOperationModeCallback callback); + + /** + * Set USB tethering. + * + * @param enabled + * Boolean to indicate we are going to enable or disable usb tethering. + * @param config + * The usb tethering configuration. + * @param callback + * Callback function used to report the result enabling/disabling usb tethering. + */ + void setUSBTethering(in boolean enabled, + in jsval config, + in nsIUsbTetheringCallback callback); + + /** + * Reset routing table. + * + * @param interfaceName + * The name of the network interface we want to remove from the routing + * table. + * + * @param callback + * Callback to notify the result of resetting routing table. + */ + void resetRoutingTable(in DOMString interfaceName, + in nsINativeCommandCallback callback); + + /** + * Set DNS. + * + * @param interfaceName + * The network interface name of the DNS we want to set. + * @param dnsesCount + * Number of elements in dnses. + * @param dnses + * Dnses to set. + * @param gatewaysCount + * Number of elements in gateways. + * @param gateways + * Gateways for the dnses, the most suitable, usually the one with the + * same address family, will be selected for each dns. + * + * @param callback + * Callback to notify the result of setting DNS server. + */ + void setDNS(in DOMString interfaceName, + in unsigned long dnsesCount, + [array, size_is(dnsesCount)] in wstring dnses, + in unsigned long gatewaysCount, + [array, size_is(gatewaysCount)] in wstring gateways, + in nsISetDnsCallback callback); + + /** + * Set default route. + * + * @param interfaceName + * The network interface name of the default route we want to set. + * @param count + * Number of elements in gateways. + * @param gateways + * Default gateways for setting default route. + * + * @param callback + * Callback to notify the result of setting default route. + */ + void setDefaultRoute(in DOMString interfaceName, + in unsigned long count, + [array, size_is(count)] in wstring gateways, + in nsINativeCommandCallback callback); + + /** + * Remove default route. + * + * @param interfaceName + * The network interface name of the default route we want to remove. + * @param count + * Number of elements in gateways. + * @param gatways + * Default gateways for removing default route. + * + * @param callback + * Callback to notify the result of removing default route. + */ + void removeDefaultRoute(in DOMString interfaceName, + in unsigned long count, + [array, size_is(count)] in wstring gateways, + in nsINativeCommandCallback callback); + + /** + * Modify route. + * + * @param action + * nsINetworkService.MODIFY_ROUTE_ADD to add route and + * nsINetworkService.MODIFY_ROUTE_REMOVE to remove. + * @param interfaceName + * Network interface name for the output of the host route. + * @param host + * Host ip we want to remove route for. + * @param prefixLength + * The prefix length of the route we'd like to modify. + * @param [optional] gateway + * Gateway ip for the output of the host route. + * + * @return A deferred promise that resolves on success or rejects with a + * specified reason otherwise. + */ + jsval modifyRoute(in long action, + in DOMString interfaceName, + in DOMString host, + in long prefixLength, + [optional] in DOMString gateway); + + /** + * Add route to secondary routing table. + * + * @param interfaceName + * The network interface for this route. + * @param route + * The route info should have the following fields: + * .ip: destination ip address + * .prefix: destination prefix + * .gateway: gateway ip address + */ + void addSecondaryRoute(in DOMString interfaceName, in jsval route, + in nsINativeCommandCallback callback); + + /** + * Remove route from secondary routing table. + * + * @param interfaceName + * The network interface for the route we want to remove. + * @param route + * The route info should have the following fields: + * .ip: destination ip address + * .prefix: destination prefix + * .gateway: gateway ip address + */ + void removeSecondaryRoute(in DOMString interfaceName, in jsval route, + in nsINativeCommandCallback callback); + + /** + * Enable or disable usb rndis. + * + * @param enable + * Boolean to indicate we want enable or disable usb rndis. + * @param callback + * Callback function to report the result. + */ + void enableUsbRndis(in boolean enable, + in nsIEnableUsbRndisCallback callback); + + /** + * Update upstream. + * + * @param previous + * The previous internal and external interface. + * @param current + * The current internal and external interface. + * @param callback + * Callback function to report the result. + */ + void updateUpStream(in jsval previous, + in jsval current, + in nsIUpdateUpStreamCallback callback); + + /* + * Obtain interfaces list. + * + * @param callback + * Callback function to return the result. + */ + void getInterfaces(in nsIGetInterfacesCallback callback); + + /** + * Get config of a network interface. + * + * @param ifname + * Target interface. + * @param callback + * Callback function to report the result. + */ + void getInterfaceConfig(in DOMString ifname, in nsIGetInterfaceConfigCallback callback); + + /** + * Set config for a network interface. + * + * @param config + * .ifname: Target interface. + * .ip: Ip address. + * .prefix: mask length. + * .link: network link properties. + * @param callback + * Callback function to report the result. + */ + void setInterfaceConfig(in jsval config, in nsISetInterfaceConfigCallback callback); + + /** + * Configure a network interface. + * + * @param config + * An object containing the detail that we want to configure the interface: + * + * - ifname: string + * - ipaddr: long + * - mask: long + * - gateway: long + * - dns1: long + * - dns2: long + * + * @param callback + * Callback to notify the result of configurating network interface. + */ + void configureInterface(in jsval config, + in nsINativeCommandCallback callback); + + /** + * Issue a DHCP client request. + * + * @param networkInterface + * The network interface which we wnat to do the DHCP request on. + * + * @param callback + * Callback to notify the result of the DHCP request. + */ + void dhcpRequest(in DOMString interfaceName, + in nsIDhcpRequestCallback callback); + + /** + * Stop Dhcp daemon. + * + * @param ifname + * Target interface. + * + * @param callback + * Callback to notify the result of stopping dhcp request. + */ + void stopDhcp(in DOMString ifname, + in nsINativeCommandCallback callback); + + /** + * Enable a network interface. + * + * @param networkInterface + * The network interface name which we want to enable. + * + * @param callback + * Callback to notify the result of disabling network interface. + */ + void enableInterface(in DOMString interfaceName, + in nsINativeCommandCallback callback); + + /** + * Disable a network interface. + * + * @param networkInterface + * The network interface name which we want to disable. + * + * @param callback + * Callback to notify the result of disabling network interface. + */ + void disableInterface(in DOMString interfaceName, + in nsINativeCommandCallback callback); + + /** + * Reset all connections on a specified network interface. + * + * @param interfaceName + * The network interface name which we want to reset. + * + * @param callback + * Callback to notify the result of resetting connections. + */ + void resetConnections(in DOMString interfaceName, + in nsINativeCommandCallback callback); + + /** + * Create network (required to call prior to any networking operation). + * + * @param interfaceName + * The network interface name which we want to create network for. + * + * @param callback + * Callback to notify the result of creating network. + */ + void createNetwork(in DOMString interfaceName, + in nsINativeCommandCallback callback); + + /** + * Destroy network. + * + * @param interfaceName + * The network interface name of the network we want to destroy. + * + * @param callback + * Callback to notify the result of destroying network. + */ + void destroyNetwork(in DOMString interfaceName, + in nsINativeCommandCallback callback); + + /** + * Query the netId associated with given network interface name. + * + * @param interfaceName + * The network interface name which we want to query. + * + * @return A deferred promise that resolves with a string to indicate. + * the queried netId on success and rejects if the interface name + * is invalid. + * + */ + jsval getNetId(in DOMString interfaceName); + + /** + * Set maximum transmission unit on a network interface. + * + * @param interfaceName + * The name of the network interface that we want to set mtu. + * @param mtu + * Size of maximum tranmission unit. + * + * @param callback + * Callback to notify the result of setting mtu. + */ + void setMtu(in DOMString interfaceName, in long mtu, + in nsINativeCommandCallback callback); +}; diff --git a/dom/system/gonk/nsINetworkWorker.idl b/dom/system/gonk/nsINetworkWorker.idl new file mode 100644 index 000000000..8fe19be69 --- /dev/null +++ b/dom/system/gonk/nsINetworkWorker.idl @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(98e31d3b-6cad-4cab-b4b3-4afff566ea65)] +interface nsINetworkEventListener : nsISupports { + void onEvent(in jsval result); +}; + +[scriptable, uuid(f9d9c694-0aac-4f9a-98ac-7788f954239a)] +interface nsINetworkWorker : nsISupports { + void start(in nsINetworkEventListener listener); + void shutdown(); + [implicit_jscontext] + void postMessage(in jsval options); +}; diff --git a/dom/system/gonk/nsIRadioInterfaceLayer.idl b/dom/system/gonk/nsIRadioInterfaceLayer.idl new file mode 100644 index 000000000..168fe3894 --- /dev/null +++ b/dom/system/gonk/nsIRadioInterfaceLayer.idl @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIIccInfo; +interface nsIMobileConnectionInfo; +interface nsIMobileMessageCallback; + +[scriptable, function, uuid(3bc96351-53b0-47a1-a888-c74c64b60f25)] +interface nsIRilSendWorkerMessageCallback : nsISupports +{ + boolean handleResponse(in jsval response); +}; + +[scriptable, uuid(1a3ef88a-e4d1-11e4-8512-176220f2b32b)] +interface nsIRadioInterface : nsISupports +{ + /** + * PDP APIs + * + * @param networkType + * Mobile network type, that is, nsINetworkInterface.NETWORK_TYPE_MOBILE + * or one of the nsINetworkInterface.NETWORK_TYPE_MOBILE_* values. + */ + void setupDataCallByType(in long networkType); + void deactivateDataCallByType(in long networkType); + long getDataCallStateByType(in long networkType); + + void updateRILNetworkInterface(); + + void sendWorkerMessage(in DOMString type, + [optional] in jsval message, + [optional] in nsIRilSendWorkerMessageCallback callback); +}; + +%{C++ +#define NS_RADIOINTERFACELAYER_CID \ + { 0x2d831c8d, 0x6017, 0x435b, \ + { 0xa8, 0x0c, 0xe5, 0xd4, 0x22, 0x81, 0x0c, 0xea } } +#define NS_RADIOINTERFACELAYER_CONTRACTID "@mozilla.org/ril;1" +%} + +[scriptable, uuid(09730e0d-75bb-4f21-8540-062a2eadc8ff)] +interface nsIRadioInterfaceLayer : nsISupports +{ + readonly attribute unsigned long numRadioInterfaces; + + nsIRadioInterface getRadioInterface(in unsigned long clientId); + + void setMicrophoneMuted(in boolean muted); +}; diff --git a/dom/system/gonk/nsISystemWorkerManager.idl b/dom/system/gonk/nsISystemWorkerManager.idl new file mode 100644 index 000000000..a77e253e4 --- /dev/null +++ b/dom/system/gonk/nsISystemWorkerManager.idl @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +/** + * Information about networks that is exposed to network manager API consumers. + */ +[scriptable, builtinclass, uuid(4984b669-0ee0-4809-ae96-3358a325a6b0)] +interface nsISystemWorkerManager : nsISupports +{ + [implicit_jscontext] + void registerRilWorker(in unsigned long aClientId, + in jsval aWorker); +}; diff --git a/dom/system/gonk/nsITetheringService.idl b/dom/system/gonk/nsITetheringService.idl new file mode 100644 index 000000000..530ab0069 --- /dev/null +++ b/dom/system/gonk/nsITetheringService.idl @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsINetworkInterface; +interface nsIWifiTetheringCallback; + +[scriptable, uuid(779de2d3-6d29-4ee6-b069-6251839f757a)] +interface nsITetheringService : nsISupports +{ + const long TETHERING_STATE_INACTIVE = 0; + const long TETHERING_STATE_WIFI = 1; + const long TETHERING_STATE_USB = 2; + + /** + * Current tethering state. One of the TETHERING_STATE_* constants. + */ + readonly attribute long state; + + /** + * Enable or disable Wifi Tethering. + * + * @param enabled + * Boolean that indicates whether tethering should be enabled (true) or + * disabled (false). + * @param interfaceName + * The Wifi network interface name for internal interface. + * @param config + * The Wifi Tethering configuration from settings db. + * @param callback + * Callback function used to report status to WifiManager. + */ + void setWifiTethering(in boolean enabled, + in DOMString interfaceName, + in jsval config, + in nsIWifiTetheringCallback callback); +};
\ No newline at end of file diff --git a/dom/system/gonk/nsIVolume.idl b/dom/system/gonk/nsIVolume.idl new file mode 100644 index 000000000..60785f0a4 --- /dev/null +++ b/dom/system/gonk/nsIVolume.idl @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIVolumeStat.idl" + +[scriptable, uuid(EE752CB8-8FD7-11E4-A602-70221D5D46B0)] +interface nsIVolume : nsISupports +{ + // These MUST match the states from android's system/vold/Volume.h header + // Note: Changes made to the STATE_xxx names should also be reflected in the + // NS_VolumeStateStr function found in Volume.cpp + const long STATE_INIT = -1; + const long STATE_NOMEDIA = 0; + const long STATE_IDLE = 1; + const long STATE_PENDING = 2; + const long STATE_CHECKING = 3; + const long STATE_MOUNTED = 4; + const long STATE_UNMOUNTING = 5; + const long STATE_FORMATTING = 6; + const long STATE_SHARED = 7; + const long STATE_SHAREDMNT = 8; + const long STATE_CHECKMNT = 100; + const long STATE_MOUNT_FAIL = 101; + + // The name of the volume. Often there is only one volume, called sdcard. + // But some phones support multiple volumes. + readonly attribute DOMString name; + + // The mount point is the path on the system where the volume is mounted + // and is only valid when state == STATE_MOUNTED. + readonly attribute DOMString mountPoint; + + // Reflects the current state of the volume, using STATE_xxx constants + // from above. + readonly attribute long state; + + // mountGeneration is a unique number which is used distinguish between + // periods of time that a volume is in the mounted state. Each time a + // volume transitions to the mounted state, the mountGeneration will + // be different from the last time it transitioned to the mounted state. + readonly attribute long mountGeneration; + + // While a volume is mounted, it can be locked, preventing it from being + // shared with the PC. To lock a volume, acquire an MozWakeLock + // using the name of this attribute. Note that mountLockName changes + // every time the mountGeneration changes, so you'll need to reacquire + // the wakelock every time the volume becomes mounted. + readonly attribute DOMString mountLockName; + + // Determines if a mountlock is currently being held against this volume. + readonly attribute boolean isMountLocked; + + // Determines if media is actually present or not. Note, that when an sdcard + // is ejected, it may go through several tranistory states before finally + // arriving at STATE_NOMEDIA. So isMediaPresent may be false even when the + // current state isn't STATE_NOMEDIA. + readonly attribute boolean isMediaPresent; + + // Determines if the volume is currently being shared. This covers off + // more than just state == STATE_SHARED. isSharing will return true from the + // time that the volume leaves the mounted state, until it gets back to + // mounted, nomedia, or formatting states. This attribute is to allow + // device storage to suppress unwanted 'unavailable' status when + // transitioning from mounted to sharing and back again. + readonly attribute boolean isSharing; + + // Determines if the volume is currently formatting. This sets true once + // mFormatRequest == true and mState == STATE_MOUNTED, and sets false + // once the volume has been formatted and mounted again. + readonly attribute boolean isFormatting; + + readonly attribute boolean isUnmounting; + + nsIVolumeStat getStats(); + + // Formats the volume in IO thread, if the volume is ready to be formatted. + // Automounter will unmount it, format it and then mount it again. + void format(); + + // Mounts the volume in IO thread, if the volume is already unmounted. + // Automounter will mount it. Otherwise Automounter will skip this. + void mount(); + + // Unmounts the volume in IO thread, if the volume is already mounted. + // Automounter will unmount it. Otherwise Automounter will skip this. + void unmount(); + + // Whether this is a fake volume. + readonly attribute boolean isFake; + + // Whether this is a removable volume + readonly attribute boolean isRemovable; + + // Whether this is a hot-swappable volume + readonly attribute boolean isHotSwappable; + +}; + +%{C++ +// For use with the ObserverService +#define NS_VOLUME_STATE_CHANGED "volume-state-changed" +#define NS_VOLUME_REMOVED "volume-removed" + +namespace mozilla { +namespace system { + +// Convert a state into a loggable/printable string. +const char* NS_VolumeStateStr(int32_t aState); + +} // system +} // mozilla +%} diff --git a/dom/system/gonk/nsIVolumeMountLock.idl b/dom/system/gonk/nsIVolumeMountLock.idl new file mode 100644 index 000000000..0a9a1a5c2 --- /dev/null +++ b/dom/system/gonk/nsIVolumeMountLock.idl @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(44449f34-5ca1-4aff-bce6-22c79263de24)] +interface nsIVolumeMountLock : nsISupports +{ + void unlock(); +}; + diff --git a/dom/system/gonk/nsIVolumeService.idl b/dom/system/gonk/nsIVolumeService.idl new file mode 100644 index 000000000..d3752e201 --- /dev/null +++ b/dom/system/gonk/nsIVolumeService.idl @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIVolume.idl" +#include "nsIVolumeMountLock.idl" + +interface nsIArray; + +[scriptable, uuid(cfbf9880-cba5-11e4-8830-0800200c9a66)] +interface nsIVolumeService : nsISupports +{ + nsIVolume getVolumeByName(in DOMString volName); + nsIVolume getVolumeByPath(in DOMString path); + nsIVolume createOrGetVolumeByPath(in DOMString path); + + nsIVolumeMountLock createMountLock(in DOMString volName); + + nsIArray getVolumeNames(); + + void Dump(in DOMString label); + + /* for test case only to simulate sdcard insertion/removal */ + void createFakeVolume(in DOMString name, in DOMString path); + void SetFakeVolumeState(in DOMString name, in long state); + + /* for test case only to test removal of storage area */ + void removeFakeVolume(in DOMString name); +}; + +%{C++ +#define NS_VOLUMESERVICE_CID \ + {0x7c179fb7, 0x67a0, 0x43a3, {0x93, 0x37, 0x29, 0x4e, 0x03, 0x60, 0xb8, 0x58}} +#define NS_VOLUMESERVICE_CONTRACTID "@mozilla.org/telephony/volume-service;1" +%} diff --git a/dom/system/gonk/nsIVolumeStat.idl b/dom/system/gonk/nsIVolumeStat.idl new file mode 100644 index 000000000..1d725689d --- /dev/null +++ b/dom/system/gonk/nsIVolumeStat.idl @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(b4c050d0-c57a-11e1-9b21-0800200c9a66)] +interface nsIVolumeStat : nsISupports +{ + readonly attribute long long totalBytes; + readonly attribute long long freeBytes; +}; diff --git a/dom/system/gonk/nsIWorkerHolder.idl b/dom/system/gonk/nsIWorkerHolder.idl new file mode 100644 index 000000000..e5cc82c9e --- /dev/null +++ b/dom/system/gonk/nsIWorkerHolder.idl @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(c04f3102-1ce8-4d57-9c27-8aece9c2740a)] +interface nsIWorkerHolder : nsISupports +{ + readonly attribute jsval worker; +}; diff --git a/dom/system/gonk/nsVolume.cpp b/dom/system/gonk/nsVolume.cpp new file mode 100644 index 000000000..77a1628e4 --- /dev/null +++ b/dom/system/gonk/nsVolume.cpp @@ -0,0 +1,467 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsVolume.h" + +#include "base/message_loop.h" +#include "base/task.h" +#include "nsIPowerManagerService.h" +#include "nsISupportsUtils.h" +#include "nsIVolume.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsVolumeStat.h" +#include "nsXULAppAPI.h" +#include "Volume.h" +#include "AutoMounter.h" +#include "VolumeManager.h" + +#undef VOLUME_MANAGER_LOG_TAG +#define VOLUME_MANAGER_LOG_TAG "nsVolume" +#include "VolumeManagerLog.h" + +namespace mozilla { +namespace system { + +const char * +NS_VolumeStateStr(int32_t aState) +{ + switch (aState) { + case nsIVolume::STATE_INIT: return "Init"; + case nsIVolume::STATE_NOMEDIA: return "NoMedia"; + case nsIVolume::STATE_IDLE: return "Idle"; + case nsIVolume::STATE_PENDING: return "Pending"; + case nsIVolume::STATE_CHECKING: return "Checking"; + case nsIVolume::STATE_MOUNTED: return "Mounted"; + case nsIVolume::STATE_UNMOUNTING: return "Unmounting"; + case nsIVolume::STATE_FORMATTING: return "Formatting"; + case nsIVolume::STATE_SHARED: return "Shared"; + case nsIVolume::STATE_SHAREDMNT: return "Shared-Mounted"; + case nsIVolume::STATE_CHECKMNT: return "Check-Mounted"; + case nsIVolume::STATE_MOUNT_FAIL: return "Mount-Fail"; + } + return "???"; +} + +// While nsVolumes can only be used on the main thread, in the +// UpdateVolumeRunnable constructor (which is called from IOThread) we +// allocate an nsVolume which is then passed to MainThread. Since we +// have a situation where we allocate on one thread and free on another +// we use a thread safe AddRef implementation. +NS_IMPL_ISUPPORTS(nsVolume, nsIVolume) + +nsVolume::nsVolume(const Volume* aVolume) + : mName(NS_ConvertUTF8toUTF16(aVolume->Name())), + mMountPoint(NS_ConvertUTF8toUTF16(aVolume->MountPoint())), + mState(aVolume->State()), + mMountGeneration(aVolume->MountGeneration()), + mMountLocked(aVolume->IsMountLocked()), + mIsFake(!aVolume->CanBeShared()), + mIsMediaPresent(aVolume->MediaPresent()), + mIsSharing(aVolume->IsSharing()), + mIsFormatting(aVolume->IsFormatting()), + mIsUnmounting(aVolume->IsUnmounting()), + mIsRemovable(aVolume->IsRemovable()), + mIsHotSwappable(aVolume->IsHotSwappable()) +{ +} + +nsVolume::nsVolume(const nsVolume* aVolume) + : mName(aVolume->mName), + mMountPoint(aVolume->mMountPoint), + mState(aVolume->mState), + mMountGeneration(aVolume->mMountGeneration), + mMountLocked(aVolume->mMountLocked), + mIsFake(aVolume->mIsFake), + mIsMediaPresent(aVolume->mIsMediaPresent), + mIsSharing(aVolume->mIsSharing), + mIsFormatting(aVolume->mIsFormatting), + mIsUnmounting(aVolume->mIsUnmounting), + mIsRemovable(aVolume->mIsRemovable), + mIsHotSwappable(aVolume->mIsHotSwappable) +{ +} + +void nsVolume::Dump(const char* aLabel) const +{ + LOG("%s: Volume: %s is %s and %s @ %s gen %d locked %d", + aLabel, + NameStr().get(), + StateStr(), + IsMediaPresent() ? "inserted" : "missing", + MountPointStr().get(), + MountGeneration(), + (int)IsMountLocked()); + LOG("%s: IsSharing %s IsFormating %s IsUnmounting %s", + aLabel, + (IsSharing() ? "y" : "n"), + (IsFormatting() ? "y" : "n"), + (IsUnmounting() ? "y" : "n")); +} + +bool nsVolume::Equals(nsIVolume* aVolume) +{ + nsString volName; + aVolume->GetName(volName); + if (!mName.Equals(volName)) { + return false; + } + + nsString volMountPoint; + aVolume->GetMountPoint(volMountPoint); + if (!mMountPoint.Equals(volMountPoint)) { + return false; + } + + int32_t volState; + aVolume->GetState(&volState); + if (mState != volState){ + return false; + } + + int32_t volMountGeneration; + aVolume->GetMountGeneration(&volMountGeneration); + if (mMountGeneration != volMountGeneration) { + return false; + } + + bool volIsMountLocked; + aVolume->GetIsMountLocked(&volIsMountLocked); + if (mMountLocked != volIsMountLocked) { + return false; + } + + bool isFake; + aVolume->GetIsFake(&isFake); + if (mIsFake != isFake) { + return false; + } + + bool isSharing; + aVolume->GetIsSharing(&isSharing); + if (mIsSharing != isSharing) { + return false; + } + + bool isFormatting; + aVolume->GetIsFormatting(&isFormatting); + if (mIsFormatting != isFormatting) { + return false; + } + + bool isUnmounting; + aVolume->GetIsUnmounting(&isUnmounting); + if (mIsUnmounting != isUnmounting) { + return false; + } + + bool isRemovable; + aVolume->GetIsRemovable(&isRemovable); + if (mIsRemovable != isRemovable) { + return false; + } + + bool isHotSwappable; + aVolume->GetIsHotSwappable(&isHotSwappable); + if (mIsHotSwappable != isHotSwappable) { + return false; + } + + return true; +} + +NS_IMETHODIMP nsVolume::GetIsMediaPresent(bool* aIsMediaPresent) +{ + *aIsMediaPresent = mIsMediaPresent; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetIsMountLocked(bool* aIsMountLocked) +{ + *aIsMountLocked = mMountLocked; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetIsSharing(bool* aIsSharing) +{ + *aIsSharing = mIsSharing; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetIsFormatting(bool* aIsFormatting) +{ + *aIsFormatting = mIsFormatting; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetIsUnmounting(bool* aIsUnmounting) +{ + *aIsUnmounting = mIsUnmounting; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetName(nsAString& aName) +{ + aName = mName; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetMountGeneration(int32_t* aMountGeneration) +{ + *aMountGeneration = mMountGeneration; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetMountLockName(nsAString& aMountLockName) +{ + aMountLockName = NS_LITERAL_STRING("volume-") + Name(); + aMountLockName.AppendPrintf("-%d", mMountGeneration); + + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetMountPoint(nsAString& aMountPoint) +{ + aMountPoint = mMountPoint; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetState(int32_t* aState) +{ + *aState = mState; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetStats(nsIVolumeStat **aResult) +{ + if (mState != STATE_MOUNTED) { + return NS_ERROR_NOT_AVAILABLE; + } + + NS_IF_ADDREF(*aResult = new nsVolumeStat(mMountPoint)); + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetIsFake(bool *aIsFake) +{ + *aIsFake = mIsFake; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetIsRemovable(bool *aIsRemovable) +{ + *aIsRemovable = mIsRemovable; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::GetIsHotSwappable(bool *aIsHotSwappable) +{ + *aIsHotSwappable = mIsHotSwappable; + return NS_OK; +} + +NS_IMETHODIMP nsVolume::Format() +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(FormatVolumeIOThread, NameStr())); + + return NS_OK; +} + +/* static */ +void nsVolume::FormatVolumeIOThread(const nsCString& aVolume) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + if (VolumeManager::State() != VolumeManager::VOLUMES_READY) { + return; + } + + AutoMounterFormatVolume(aVolume); +} + +NS_IMETHODIMP nsVolume::Mount() +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(MountVolumeIOThread, NameStr())); + + return NS_OK; +} + +/* static */ +void nsVolume::MountVolumeIOThread(const nsCString& aVolume) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + if (VolumeManager::State() != VolumeManager::VOLUMES_READY) { + return; + } + + AutoMounterMountVolume(aVolume); +} + +NS_IMETHODIMP nsVolume::Unmount() +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(UnmountVolumeIOThread, NameStr())); + + return NS_OK; +} + +/* static */ +void nsVolume::UnmountVolumeIOThread(const nsCString& aVolume) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + if (VolumeManager::State() != VolumeManager::VOLUMES_READY) { + return; + } + + AutoMounterUnmountVolume(aVolume); +} + +void +nsVolume::LogState() const +{ + if (mState == nsIVolume::STATE_MOUNTED) { + LOG("nsVolume: %s state %s @ '%s' gen %d locked %d fake %d " + "media %d sharing %d formatting %d unmounting %d removable %d hotswappable %d", + NameStr().get(), StateStr(), MountPointStr().get(), + MountGeneration(), (int)IsMountLocked(), (int)IsFake(), + (int)IsMediaPresent(), (int)IsSharing(), + (int)IsFormatting(), (int)IsUnmounting(), + (int)IsRemovable(), (int)IsHotSwappable()); + return; + } + + LOG("nsVolume: %s state %s", NameStr().get(), StateStr()); +} + +void nsVolume::UpdateMountLock(nsVolume* aOldVolume) +{ + MOZ_ASSERT(NS_IsMainThread()); + + bool oldMountLocked = aOldVolume ? aOldVolume->mMountLocked : false; + if (mState != nsIVolume::STATE_MOUNTED) { + // Since we're not in the mounted state, we need to + // forgot whatever mount generation we may have had. + mMountGeneration = -1; + mMountLocked = oldMountLocked; + return; + } + + int32_t oldMountGeneration = aOldVolume ? aOldVolume->mMountGeneration : -1; + if (mMountGeneration == oldMountGeneration) { + // No change in mount generation, nothing else to do + mMountLocked = oldMountLocked; + return; + } + + if (!XRE_IsParentProcess()) { + // Child processes just track the state, not maintain it. + return; + } + + // Notify the Volume on IOThread whether the volume is locked or not. + nsCOMPtr<nsIPowerManagerService> pmService = + do_GetService(POWERMANAGERSERVICE_CONTRACTID); + if (!pmService) { + return; + } + nsString mountLockName; + GetMountLockName(mountLockName); + nsString mountLockState; + pmService->GetWakeLockState(mountLockName, mountLockState); + UpdateMountLock(mountLockState); +} + +void +nsVolume::UpdateMountLock(const nsAString& aMountLockState) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + // There are 3 states, unlocked, locked-background, and locked-foreground + // I figured it was easier to use negtive logic and compare for unlocked. + UpdateMountLock(!aMountLockState.EqualsLiteral("unlocked")); +} + +void +nsVolume::UpdateMountLock(bool aMountLocked) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + if (aMountLocked == mMountLocked) { + return; + } + // The locked/unlocked state changed. Tell IOThread about it. + mMountLocked = aMountLocked; + LogState(); + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(Volume::UpdateMountLock, + NS_LossyConvertUTF16toASCII(Name()), + MountGeneration(), aMountLocked)); +} + +void +nsVolume::SetIsFake(bool aIsFake) +{ + mIsFake = aIsFake; + if (mIsFake) { + // The media is always present for fake volumes. + mIsMediaPresent = true; + MOZ_ASSERT(!mIsSharing); + } +} + +void +nsVolume::SetIsRemovable(bool aIsRemovable) +{ + mIsRemovable = aIsRemovable; + if (!mIsRemovable) { + mIsHotSwappable = false; + } +} + +void +nsVolume::SetIsHotSwappable(bool aIsHotSwappable) +{ + mIsHotSwappable = aIsHotSwappable; + if (mIsHotSwappable) { + mIsRemovable = true; + } +} + +void +nsVolume::SetState(int32_t aState) +{ + static int32_t sMountGeneration = 0; + + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsFake()); + + if (aState == mState) { + return; + } + + if (aState == nsIVolume::STATE_MOUNTED) { + mMountGeneration = ++sMountGeneration; + } + + mState = aState; +} + +} // system +} // mozilla diff --git a/dom/system/gonk/nsVolume.h b/dom/system/gonk/nsVolume.h new file mode 100644 index 000000000..88be425f6 --- /dev/null +++ b/dom/system/gonk/nsVolume.h @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_nsvolume_h__ +#define mozilla_system_nsvolume_h__ + +#include "nsCOMPtr.h" +#include "nsIVolume.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +namespace system { + +class Volume; + +class nsVolume : public nsIVolume +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIVOLUME + + // This constructor is used by the UpdateVolumeRunnable constructor + nsVolume(const Volume* aVolume); + + // This constructor is used by nsVolumeService::SetFakeVolumeState + nsVolume(const nsVolume* aVolume); + + // This constructor is used by ContentChild::RecvFileSystemUpdate which is + // used to update the volume cache maintained in the child process. + nsVolume(const nsAString& aName, const nsAString& aMountPoint, + const int32_t& aState, const int32_t& aMountGeneration, + const bool& aIsMediaPresent, const bool& aIsSharing, + const bool& aIsFormatting, const bool& aIsFake, + const bool& aIsUnmounting, const bool& aIsRemovable, + const bool& aIsHotSwappable) + : mName(aName), + mMountPoint(aMountPoint), + mState(aState), + mMountGeneration(aMountGeneration), + mMountLocked(false), + mIsFake(aIsFake), + mIsMediaPresent(aIsMediaPresent), + mIsSharing(aIsSharing), + mIsFormatting(aIsFormatting), + mIsUnmounting(aIsUnmounting), + mIsRemovable(aIsRemovable), + mIsHotSwappable(aIsHotSwappable) + { + } + + bool Equals(nsIVolume* aVolume); + void UpdateMountLock(nsVolume* aOldVolume); + + void LogState() const; + + const nsString& Name() const { return mName; } + nsCString NameStr() const { return NS_LossyConvertUTF16toASCII(mName); } + + void Dump(const char* aLabel) const; + + int32_t MountGeneration() const { return mMountGeneration; } + bool IsMountLocked() const { return mMountLocked; } + + const nsString& MountPoint() const { return mMountPoint; } + nsCString MountPointStr() const { return NS_LossyConvertUTF16toASCII(mMountPoint); } + + int32_t State() const { return mState; } + const char* StateStr() const { return NS_VolumeStateStr(mState); } + + bool IsFake() const { return mIsFake; } + bool IsMediaPresent() const { return mIsMediaPresent; } + bool IsSharing() const { return mIsSharing; } + bool IsFormatting() const { return mIsFormatting; } + bool IsUnmounting() const { return mIsUnmounting; } + bool IsRemovable() const { return mIsRemovable; } + bool IsHotSwappable() const { return mIsHotSwappable; } + + typedef nsTArray<RefPtr<nsVolume> > Array; + +private: + virtual ~nsVolume() {} // MozExternalRefCountType complains if this is non-virtual + + friend class nsVolumeService; // Calls the following XxxMountLock functions + void UpdateMountLock(const nsAString& aMountLockState); + void UpdateMountLock(bool aMountLocked); + + void SetIsFake(bool aIsFake); + void SetIsRemovable(bool aIsRemovable); + void SetIsHotSwappable(bool aIsHotSwappble); + void SetState(int32_t aState); + static void FormatVolumeIOThread(const nsCString& aVolume); + static void MountVolumeIOThread(const nsCString& aVolume); + static void UnmountVolumeIOThread(const nsCString& aVolume); + + nsString mName; + nsString mMountPoint; + int32_t mState; + int32_t mMountGeneration; + bool mMountLocked; + bool mIsFake; + bool mIsMediaPresent; + bool mIsSharing; + bool mIsFormatting; + bool mIsUnmounting; + bool mIsRemovable; + bool mIsHotSwappable; +}; + +} // system +} // mozilla + +#endif // mozilla_system_nsvolume_h__ diff --git a/dom/system/gonk/nsVolumeMountLock.cpp b/dom/system/gonk/nsVolumeMountLock.cpp new file mode 100644 index 000000000..288c0f689 --- /dev/null +++ b/dom/system/gonk/nsVolumeMountLock.cpp @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsVolumeMountLock.h" + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/Services.h" + +#include "nsIObserverService.h" +#include "nsIPowerManagerService.h" +#include "nsIVolume.h" +#include "nsIVolumeService.h" +#include "nsString.h" +#include "nsXULAppAPI.h" + +#undef VOLUME_MANAGER_LOG_TAG +#define VOLUME_MANAGER_LOG_TAG "nsVolumeMountLock" +#include "VolumeManagerLog.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/power/PowerManagerService.h" + +using namespace mozilla::dom; +using namespace mozilla::services; + +namespace mozilla { +namespace system { + +NS_IMPL_ISUPPORTS(nsVolumeMountLock, nsIVolumeMountLock, + nsIObserver, nsISupportsWeakReference) + +// static +already_AddRefed<nsVolumeMountLock> +nsVolumeMountLock::Create(const nsAString& aVolumeName) +{ + DBG("nsVolumeMountLock::Create called"); + + RefPtr<nsVolumeMountLock> mountLock = new nsVolumeMountLock(aVolumeName); + nsresult rv = mountLock->Init(); + NS_ENSURE_SUCCESS(rv, nullptr); + + return mountLock.forget(); +} + +nsVolumeMountLock::nsVolumeMountLock(const nsAString& aVolumeName) + : mVolumeName(aVolumeName), + mVolumeGeneration(-1), + mUnlocked(false) +{ +} + +//virtual +nsVolumeMountLock::~nsVolumeMountLock() +{ + Unlock(); +} + +nsresult nsVolumeMountLock::Init() +{ + LOG("nsVolumeMountLock created for '%s'", + NS_LossyConvertUTF16toASCII(mVolumeName).get()); + + // Add ourselves as an Observer. It's important that we use a weak + // reference here. If we used a strong reference, then that reference + // would prevent this object from being destructed. + nsCOMPtr<nsIObserverService> obs = GetObserverService(); + obs->AddObserver(this, NS_VOLUME_STATE_CHANGED, true /*weak*/); + + // Get the initial mountGeneration and grab a lock. + nsCOMPtr<nsIVolumeService> vs = do_GetService(NS_VOLUMESERVICE_CONTRACTID); + NS_ENSURE_TRUE(vs, NS_ERROR_FAILURE); + + nsCOMPtr<nsIVolume> vol; + nsresult rv = vs->GetVolumeByName(mVolumeName, getter_AddRefs(vol)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = vol->GetMountGeneration(&mVolumeGeneration); + NS_ENSURE_SUCCESS(rv, rv); + + return Lock(vol); +} + +NS_IMETHODIMP nsVolumeMountLock::Unlock() +{ + LOG("nsVolumeMountLock released for '%s'", + NS_LossyConvertUTF16toASCII(mVolumeName).get()); + + mUnlocked = true; + mWakeLock = nullptr; + + // While we don't really need to remove weak observers, we do so anyways + // since it will reduce the number of times Observe gets called. + nsCOMPtr<nsIObserverService> obs = GetObserverService(); + obs->RemoveObserver(this, NS_VOLUME_STATE_CHANGED); + return NS_OK; +} + +NS_IMETHODIMP nsVolumeMountLock::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) +{ + if (strcmp(aTopic, NS_VOLUME_STATE_CHANGED) != 0) { + return NS_OK; + } + if (mUnlocked) { + // We're not locked anymore, so we don't need to look at the notifications. + return NS_OK; + } + + nsCOMPtr<nsIVolume> vol = do_QueryInterface(aSubject); + if (!vol) { + return NS_OK; + } + nsString volName; + vol->GetName(volName); + if (!volName.Equals(mVolumeName)) { + return NS_OK; + } + int32_t state; + nsresult rv = vol->GetState(&state); + NS_ENSURE_SUCCESS(rv, rv); + + if (state != nsIVolume::STATE_MOUNTED) { + mWakeLock = nullptr; + mVolumeGeneration = -1; + return NS_OK; + } + + int32_t mountGeneration; + rv = vol->GetMountGeneration(&mountGeneration); + NS_ENSURE_SUCCESS(rv, rv); + + DBG("nsVolumeMountLock::Observe mountGeneration = %d mVolumeGeneration = %d", + mountGeneration, mVolumeGeneration); + + if (mVolumeGeneration == mountGeneration) { + return NS_OK; + } + + // The generation changed, which means that any wakelock we may have + // been holding is now invalid. Grab a new wakelock for the new generation + // number. + + mWakeLock = nullptr; + mVolumeGeneration = mountGeneration; + + return Lock(vol); +} + +nsresult +nsVolumeMountLock::Lock(nsIVolume* aVolume) +{ + RefPtr<power::PowerManagerService> pmService = + power::PowerManagerService::GetInstance(); + NS_ENSURE_TRUE(pmService, NS_ERROR_FAILURE); + + nsString mountLockName; + aVolume->GetMountLockName(mountLockName); + + ErrorResult err; + mWakeLock = pmService->NewWakeLock(mountLockName, nullptr, err); + if (err.Failed()) { + return err.StealNSResult(); + } + + LOG("nsVolumeMountLock acquired for '%s' gen %d", + NS_LossyConvertUTF16toASCII(mVolumeName).get(), mVolumeGeneration); + return NS_OK; +} + +} // namespace system +} // namespace mozilla diff --git a/dom/system/gonk/nsVolumeMountLock.h b/dom/system/gonk/nsVolumeMountLock.h new file mode 100644 index 000000000..caf5b2ad5 --- /dev/null +++ b/dom/system/gonk/nsVolumeMountLock.h @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_nsvolumemountlock_h__ +#define mozilla_system_nsvolumemountlock_h__ + +#include "nsIVolumeMountLock.h" + +#include "mozilla/dom/WakeLock.h" +#include "nsIObserver.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWeakReference.h" + +class nsIVolume; + +namespace mozilla { +namespace system { + +/* The VolumeMountLock is designed so that it can be used in the Child or + * Parent process. While the VolumeMountLock object exists, then the + * VolumeManager/AutoMounter will prevent a mounted volume from being + * shared with the PC. + */ + +class nsVolumeMountLock final : public nsIVolumeMountLock, + public nsIObserver, + public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIVOLUMEMOUNTLOCK + + static already_AddRefed<nsVolumeMountLock> Create(const nsAString& volumeName); + + const nsString& VolumeName() const { return mVolumeName; } + +private: + nsVolumeMountLock(const nsAString& aVolumeName); + ~nsVolumeMountLock(); + + nsresult Init(); + nsresult Lock(nsIVolume* aVolume); + + RefPtr<dom::WakeLock> mWakeLock; + nsString mVolumeName; + int32_t mVolumeGeneration; + bool mUnlocked; +}; + +} // namespace system +} // namespace mozilla + +#endif // mozilla_system_nsvolumemountlock_h__ diff --git a/dom/system/gonk/nsVolumeService.cpp b/dom/system/gonk/nsVolumeService.cpp new file mode 100644 index 000000000..48d95c26a --- /dev/null +++ b/dom/system/gonk/nsVolumeService.cpp @@ -0,0 +1,553 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsVolumeService.h" + +#include "Volume.h" +#include "VolumeManager.h" +#include "VolumeServiceIOThread.h" + +#include "nsCOMPtr.h" +#include "nsDependentSubstring.h" +#include "nsIDOMWakeLockListener.h" +#include "nsIMutableArray.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIPowerManagerService.h" +#include "nsISupportsPrimitives.h" +#include "nsISupportsUtils.h" +#include "nsIVolume.h" +#include "nsIVolumeService.h" +#include "nsLocalFile.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsVolumeMountLock.h" +#include "nsXULAppAPI.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/Services.h" +#include "base/task.h" + +#undef VOLUME_MANAGER_LOG_TAG +#define VOLUME_MANAGER_LOG_TAG "nsVolumeService" +#include "VolumeManagerLog.h" + +#include <stdlib.h> + +using namespace mozilla::dom; +using namespace mozilla::services; + +namespace mozilla { +namespace system { + +NS_IMPL_ISUPPORTS(nsVolumeService, + nsIVolumeService, + nsIDOMMozWakeLockListener) + +StaticRefPtr<nsVolumeService> nsVolumeService::sSingleton; + +// static +already_AddRefed<nsVolumeService> +nsVolumeService::GetSingleton() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!sSingleton) { + sSingleton = new nsVolumeService(); + } + RefPtr<nsVolumeService> volumeService = sSingleton.get(); + return volumeService.forget(); +} + +// static +void +nsVolumeService::Shutdown() +{ + if (!sSingleton) { + return; + } + if (!XRE_IsParentProcess()) { + sSingleton = nullptr; + return; + } + + nsCOMPtr<nsIPowerManagerService> pmService = + do_GetService(POWERMANAGERSERVICE_CONTRACTID); + if (pmService) { + pmService->RemoveWakeLockListener(sSingleton.get()); + } + + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(ShutdownVolumeServiceIOThread)); + + sSingleton = nullptr; +} + +nsVolumeService::nsVolumeService() + : mArrayMonitor("nsVolumeServiceArray"), + mGotVolumesFromParent(false) +{ + sSingleton = this; + + if (!XRE_IsParentProcess()) { + // VolumeServiceIOThread and the WakeLock listener should only run in the + // parent, so we return early. + return; + } + + // Startup the IOThread side of things. The actual volume changes + // are captured by the IOThread and forwarded to main thread. + XRE_GetIOMessageLoop()->PostTask( + NewRunnableFunction(InitVolumeServiceIOThread, this)); + + nsCOMPtr<nsIPowerManagerService> pmService = + do_GetService(POWERMANAGERSERVICE_CONTRACTID); + if (!pmService) { + return; + } + pmService->AddWakeLockListener(this); +} + +nsVolumeService::~nsVolumeService() +{ +} + +// Callback for nsIDOMMozWakeLockListener +NS_IMETHODIMP +nsVolumeService::Callback(const nsAString& aTopic, const nsAString& aState) +{ + CheckMountLock(aTopic, aState); + return NS_OK; +} + +void nsVolumeService::DumpNoLock(const char* aLabel) +{ + mArrayMonitor.AssertCurrentThreadOwns(); + + nsVolume::Array::size_type numVolumes = mVolumeArray.Length(); + + if (numVolumes == 0) { + LOG("%s: No Volumes!", aLabel); + return; + } + nsVolume::Array::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<nsVolume> vol = mVolumeArray[volIndex]; + vol->Dump(aLabel); + } +} + +NS_IMETHODIMP +nsVolumeService::Dump(const nsAString& aLabel) +{ + MonitorAutoLock autoLock(mArrayMonitor); + DumpNoLock(NS_LossyConvertUTF16toASCII(aLabel).get()); + return NS_OK; +} + +NS_IMETHODIMP nsVolumeService::GetVolumeByName(const nsAString& aVolName, nsIVolume **aResult) +{ + MonitorAutoLock autoLock(mArrayMonitor); + + RefPtr<nsVolume> vol = FindVolumeByName(aVolName); + if (!vol) { + return NS_ERROR_NOT_AVAILABLE; + } + + vol.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsVolumeService::GetVolumeByPath(const nsAString& aPath, nsIVolume **aResult) +{ + NS_ConvertUTF16toUTF8 utf8Path(aPath); + char realPathBuf[PATH_MAX]; + + while (realpath(utf8Path.get(), realPathBuf) < 0) { + if (errno != ENOENT) { + ERR("GetVolumeByPath: realpath on '%s' failed: %d", utf8Path.get(), errno); + return NSRESULT_FOR_ERRNO(); + } + // The pathname we were passed doesn't exist, so we try stripping off trailing + // components until we get a successful call to realpath, or until we run out + // of components (if we finally get to /something then we also stop). + int32_t slashIndex = utf8Path.RFindChar('/'); + if ((slashIndex == kNotFound) || (slashIndex == 0)) { + errno = ENOENT; + ERR("GetVolumeByPath: realpath on '%s' failed.", utf8Path.get()); + return NSRESULT_FOR_ERRNO(); + } + utf8Path.Assign(Substring(utf8Path, 0, slashIndex)); + } + + // The volume mount point is always a directory. Something like /mnt/sdcard + // Once we have a full qualified pathname with symlinks removed (which is + // what realpath does), we basically check if aPath starts with the mount + // point, but we don't want to have /mnt/sdcard match /mnt/sdcardfoo but we + // do want it to match /mnt/sdcard/foo + // So we add a trailing slash to the mount point and the pathname passed in + // prior to doing the comparison. + + strlcat(realPathBuf, "/", sizeof(realPathBuf)); + + MonitorAutoLock autoLock(mArrayMonitor); + + nsVolume::Array::size_type numVolumes = mVolumeArray.Length(); + nsVolume::Array::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<nsVolume> vol = mVolumeArray[volIndex]; + NS_ConvertUTF16toUTF8 volMountPointSlash(vol->MountPoint()); + volMountPointSlash.Append('/'); + nsDependentCSubstring testStr(realPathBuf, volMountPointSlash.Length()); + if (volMountPointSlash.Equals(testStr)) { + vol.forget(aResult); + return NS_OK; + } + } + return NS_ERROR_FILE_NOT_FOUND; +} + +NS_IMETHODIMP +nsVolumeService::CreateOrGetVolumeByPath(const nsAString& aPath, nsIVolume** aResult) +{ + nsresult rv = GetVolumeByPath(aPath, aResult); + if (rv == NS_OK) { + return NS_OK; + } + + // In order to support queries by the updater, we will fabricate a volume + // from the pathname, so that the caller can determine the volume size. + nsCOMPtr<nsIVolume> vol = new nsVolume(NS_LITERAL_STRING("fake"), + aPath, nsIVolume::STATE_MOUNTED, + -1 /* generation */, + true /* isMediaPresent*/, + false /* isSharing */, + false /* isFormatting */, + true /* isFake */, + false /* isUnmounting */, + false /* isRemovable */, + false /* isHotSwappable*/); + vol.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsVolumeService::GetVolumeNames(nsIArray** aVolNames) +{ + NS_ENSURE_ARG_POINTER(aVolNames); + MonitorAutoLock autoLock(mArrayMonitor); + + *aVolNames = nullptr; + + nsresult rv; + nsCOMPtr<nsIMutableArray> volNames = + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsVolume::Array::size_type numVolumes = mVolumeArray.Length(); + nsVolume::Array::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<nsVolume> vol = mVolumeArray[volIndex]; + nsCOMPtr<nsISupportsString> isupportsString = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = isupportsString->SetData(vol->Name()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = volNames->AppendElement(isupportsString, false); + NS_ENSURE_SUCCESS(rv, rv); + } + + volNames.forget(aVolNames); + return NS_OK; +} + +void +nsVolumeService::GetVolumesForIPC(nsTArray<VolumeInfo>* aResult) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + MonitorAutoLock autoLock(mArrayMonitor); + + nsVolume::Array::size_type numVolumes = mVolumeArray.Length(); + nsVolume::Array::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<nsVolume> vol = mVolumeArray[volIndex]; + VolumeInfo* volInfo = aResult->AppendElement(); + + volInfo->name() = vol->mName; + volInfo->mountPoint() = vol->mMountPoint; + volInfo->volState() = vol->mState; + volInfo->mountGeneration() = vol->mMountGeneration; + volInfo->isMediaPresent() = vol->mIsMediaPresent; + volInfo->isSharing() = vol->mIsSharing; + volInfo->isFormatting() = vol->mIsFormatting; + volInfo->isFake() = vol->mIsFake; + volInfo->isUnmounting() = vol->mIsUnmounting; + volInfo->isRemovable() = vol->mIsRemovable; + volInfo->isHotSwappable() = vol->mIsHotSwappable; + } +} + +void +nsVolumeService::RecvVolumesFromParent(const nsTArray<VolumeInfo>& aVolumes) +{ + if (XRE_IsParentProcess()) { + // We are the parent. Therefore our volumes are already correct. + return; + } + if (mGotVolumesFromParent) { + // We've already done this, no need to do it again. + return; + } + + for (uint32_t i = 0; i < aVolumes.Length(); i++) { + const VolumeInfo& volInfo(aVolumes[i]); + RefPtr<nsVolume> vol = new nsVolume(volInfo.name(), + volInfo.mountPoint(), + volInfo.volState(), + volInfo.mountGeneration(), + volInfo.isMediaPresent(), + volInfo.isSharing(), + volInfo.isFormatting(), + volInfo.isFake(), + volInfo.isUnmounting(), + volInfo.isRemovable(), + volInfo.isHotSwappable()); + UpdateVolume(vol, false); + } +} + +NS_IMETHODIMP +nsVolumeService::CreateMountLock(const nsAString& aVolumeName, nsIVolumeMountLock **aResult) +{ + nsCOMPtr<nsIVolumeMountLock> mountLock = nsVolumeMountLock::Create(aVolumeName); + if (!mountLock) { + return NS_ERROR_NOT_AVAILABLE; + } + mountLock.forget(aResult); + return NS_OK; +} + +void +nsVolumeService::CheckMountLock(const nsAString& aMountLockName, + const nsAString& aMountLockState) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<nsVolume> vol = FindVolumeByMountLockName(aMountLockName); + if (vol) { + vol->UpdateMountLock(aMountLockState); + } +} + +already_AddRefed<nsVolume> +nsVolumeService::FindVolumeByMountLockName(const nsAString& aMountLockName) +{ + MonitorAutoLock autoLock(mArrayMonitor); + + nsVolume::Array::size_type numVolumes = mVolumeArray.Length(); + nsVolume::Array::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<nsVolume> vol = mVolumeArray[volIndex]; + nsString mountLockName; + vol->GetMountLockName(mountLockName); + if (mountLockName.Equals(aMountLockName)) { + return vol.forget(); + } + } + return nullptr; +} + +already_AddRefed<nsVolume> +nsVolumeService::FindVolumeByName(const nsAString& aName, nsVolume::Array::index_type* aIndex) +{ + mArrayMonitor.AssertCurrentThreadOwns(); + + nsVolume::Array::size_type numVolumes = mVolumeArray.Length(); + nsVolume::Array::index_type volIndex; + for (volIndex = 0; volIndex < numVolumes; volIndex++) { + RefPtr<nsVolume> vol = mVolumeArray[volIndex]; + if (vol->Name().Equals(aName)) { + if (aIndex) { + *aIndex = volIndex; + } + return vol.forget(); + } + } + return nullptr; +} + +void +nsVolumeService::UpdateVolume(nsVolume* aVolume, bool aNotifyObservers) +{ + MOZ_ASSERT(NS_IsMainThread()); + + { + MonitorAutoLock autoLock(mArrayMonitor); + nsVolume::Array::index_type volIndex; + RefPtr<nsVolume> vol = FindVolumeByName(aVolume->Name(), &volIndex); + if (!vol) { + mVolumeArray.AppendElement(aVolume); + } else if (vol->Equals(aVolume) || (!vol->IsFake() && aVolume->IsFake())) { + // Ignore if nothing changed or if a fake tries to override a real volume. + return; + } else { + mVolumeArray.ReplaceElementAt(volIndex, aVolume); + } + aVolume->UpdateMountLock(vol); + } + + if (!aNotifyObservers) { + return; + } + + nsCOMPtr<nsIObserverService> obs = GetObserverService(); + if (!obs) { + return; + } + NS_ConvertUTF8toUTF16 stateStr(aVolume->StateStr()); + obs->NotifyObservers(aVolume, NS_VOLUME_STATE_CHANGED, stateStr.get()); +} + +NS_IMETHODIMP +nsVolumeService::CreateFakeVolume(const nsAString& name, const nsAString& path) +{ + if (XRE_IsParentProcess()) { + RefPtr<nsVolume> vol = new nsVolume(name, path, nsIVolume::STATE_INIT, + -1 /* mountGeneration */, + true /* isMediaPresent */, + false /* isSharing */, + false /* isFormatting */, + true /* isFake */, + false /* isUnmounting */, + false /* isRemovable */, + false /* isHotSwappable */); + vol->SetState(nsIVolume::STATE_MOUNTED); + vol->LogState(); + UpdateVolume(vol.get()); + return NS_OK; + } + + ContentChild::GetSingleton()->SendCreateFakeVolume(nsString(name), nsString(path)); + return NS_OK; +} + +NS_IMETHODIMP +nsVolumeService::SetFakeVolumeState(const nsAString& name, int32_t state) +{ + if (XRE_IsParentProcess()) { + RefPtr<nsVolume> vol; + { + MonitorAutoLock autoLock(mArrayMonitor); + vol = FindVolumeByName(name); + } + if (!vol || !vol->IsFake()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Clone the existing volume so we can replace it + RefPtr<nsVolume> volume = new nsVolume(vol); + volume->SetState(state); + volume->LogState(); + UpdateVolume(volume.get()); + return NS_OK; + } + + ContentChild::GetSingleton()->SendSetFakeVolumeState(nsString(name), state); + return NS_OK; +} + +NS_IMETHODIMP +nsVolumeService::RemoveFakeVolume(const nsAString& name) +{ + if (XRE_IsParentProcess()) { + SetFakeVolumeState(name, nsIVolume::STATE_NOMEDIA); + RemoveVolumeByName(name); + return NS_OK; + } + + ContentChild::GetSingleton()->SendRemoveFakeVolume(nsString(name)); + return NS_OK; +} + +void +nsVolumeService::RemoveVolumeByName(const nsAString& aName) +{ + { + MonitorAutoLock autoLock(mArrayMonitor); + nsVolume::Array::index_type volIndex; + RefPtr<nsVolume> vol = FindVolumeByName(aName, &volIndex); + if (!vol) { + return; + } + mVolumeArray.RemoveElementAt(volIndex); + } + + if (XRE_IsParentProcess()) { + nsCOMPtr<nsIObserverService> obs = GetObserverService(); + if (!obs) { + return; + } + obs->NotifyObservers(nullptr, NS_VOLUME_REMOVED, nsString(aName).get()); + } +} + +/*************************************************************************** +* The UpdateVolumeRunnable creates an nsVolume and updates the main thread +* data structure while running on the main thread. +*/ +class UpdateVolumeRunnable : public Runnable +{ +public: + UpdateVolumeRunnable(nsVolumeService* aVolumeService, const Volume* aVolume) + : mVolumeService(aVolumeService), + mVolume(new nsVolume(aVolume)) + { + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + DBG("UpdateVolumeRunnable::Run '%s' state %s gen %d locked %d " + "media %d sharing %d formatting %d unmounting %d removable %d hotswappable %d", + mVolume->NameStr().get(), mVolume->StateStr(), + mVolume->MountGeneration(), (int)mVolume->IsMountLocked(), + (int)mVolume->IsMediaPresent(), mVolume->IsSharing(), + mVolume->IsFormatting(), mVolume->IsUnmounting(), + (int)mVolume->IsRemovable(), (int)mVolume->IsHotSwappable()); + + mVolumeService->UpdateVolume(mVolume); + mVolumeService = nullptr; + mVolume = nullptr; + return NS_OK; + } + +private: + RefPtr<nsVolumeService> mVolumeService; + RefPtr<nsVolume> mVolume; +}; + +void +nsVolumeService::UpdateVolumeIOThread(const Volume* aVolume) +{ + DBG("UpdateVolumeIOThread: Volume '%s' state %s mount '%s' gen %d locked %d " + "media %d sharing %d formatting %d unmounting %d removable %d hotswappable %d", + aVolume->NameStr(), aVolume->StateStr(), aVolume->MountPoint().get(), + aVolume->MountGeneration(), (int)aVolume->IsMountLocked(), + (int)aVolume->MediaPresent(), (int)aVolume->IsSharing(), + (int)aVolume->IsFormatting(), (int)aVolume->IsUnmounting(), + (int)aVolume->IsRemovable(), (int)aVolume->IsHotSwappable()); + MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); + NS_DispatchToMainThread(new UpdateVolumeRunnable(this, aVolume)); +} + +} // namespace system +} // namespace mozilla diff --git a/dom/system/gonk/nsVolumeService.h b/dom/system/gonk/nsVolumeService.h new file mode 100644 index 000000000..9bddc0b8f --- /dev/null +++ b/dom/system/gonk/nsVolumeService.h @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_nsvolumeservice_h__ +#define mozilla_system_nsvolumeservice_h__ + +#include "mozilla/Monitor.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticPtr.h" +#include "nsCOMPtr.h" +#include "nsIDOMWakeLockListener.h" +#include "nsIVolume.h" +#include "nsIVolumeService.h" +#include "nsVolume.h" + +namespace mozilla { + +namespace dom { +class VolumeInfo; +} // dom + +namespace system { + +class Volume; + +/*************************************************************************** +* The nsVolumeData class encapsulates the data that is updated/maintained +* on the main thread in order to support the nsIVolume and nsIVolumeService +* classes. +*/ + +class nsVolumeService final : public nsIVolumeService, + public nsIDOMMozWakeLockListener +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIVOLUMESERVICE + NS_DECL_NSIDOMMOZWAKELOCKLISTENER + + nsVolumeService(); + + static already_AddRefed<nsVolumeService> GetSingleton(); + //static nsVolumeService* GetSingleton(); + static void Shutdown(); + + void DumpNoLock(const char* aLabel); + + // To use this function, you have to create a new volume and pass it in. + void UpdateVolume(nsVolume* aVolume, bool aNotifyObservers = true); + void UpdateVolumeIOThread(const Volume* aVolume); + + void RecvVolumesFromParent(const nsTArray<dom::VolumeInfo>& aVolumes); + void GetVolumesForIPC(nsTArray<dom::VolumeInfo>* aResult); + + void RemoveVolumeByName(const nsAString& aName); + +private: + ~nsVolumeService(); + + void CheckMountLock(const nsAString& aMountLockName, + const nsAString& aMountLockState); + already_AddRefed<nsVolume> FindVolumeByMountLockName(const nsAString& aMountLockName); + + already_AddRefed<nsVolume> FindVolumeByName(const nsAString& aName, + nsVolume::Array::index_type* aIndex = nullptr); + + Monitor mArrayMonitor; + nsVolume::Array mVolumeArray; + + static StaticRefPtr<nsVolumeService> sSingleton; + bool mGotVolumesFromParent; +}; + +} // system +} // mozilla + +#endif // mozilla_system_nsvolumeservice_h__ diff --git a/dom/system/gonk/nsVolumeStat.cpp b/dom/system/gonk/nsVolumeStat.cpp new file mode 100644 index 000000000..11976237f --- /dev/null +++ b/dom/system/gonk/nsVolumeStat.cpp @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsVolumeStat.h" +#include "nsString.h" + +namespace mozilla { +namespace system { + +NS_IMPL_ISUPPORTS(nsVolumeStat, nsIVolumeStat) + +nsVolumeStat::nsVolumeStat(const nsAString& aPath) +{ + if (statfs(NS_ConvertUTF16toUTF8(aPath).get(), &mStat) != 0) { + memset(&mStat, 0, sizeof(mStat)); + } +} + +NS_IMETHODIMP nsVolumeStat::GetTotalBytes(int64_t* aTotalBytes) +{ + *aTotalBytes = mStat.f_blocks * mStat.f_bsize; + return NS_OK; +} + +NS_IMETHODIMP nsVolumeStat::GetFreeBytes(int64_t* aFreeBytes) +{ + *aFreeBytes = mStat.f_bfree * mStat.f_bsize; + return NS_OK; +} + +} // system +} // mozilla diff --git a/dom/system/gonk/nsVolumeStat.h b/dom/system/gonk/nsVolumeStat.h new file mode 100644 index 000000000..2ca03ed46 --- /dev/null +++ b/dom/system/gonk/nsVolumeStat.h @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_system_nsvolumestat_h__ +#define mozilla_system_nsvolumestat_h__ + +#include "nsIVolumeStat.h" +#include "nsString.h" +#include <sys/statfs.h> + +namespace mozilla { +namespace system { + +class nsVolumeStat final : public nsIVolumeStat +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIVOLUMESTAT + + nsVolumeStat(const nsAString& aPath); + +protected: + ~nsVolumeStat() {} + +private: + struct statfs mStat; +}; + +} // system +} // mozilla + +#endif // mozilla_system_nsvolumestat_h__ diff --git a/dom/system/gonk/ril_consts.js b/dom/system/gonk/ril_consts.js new file mode 100644 index 000000000..af5b9d8e1 --- /dev/null +++ b/dom/system/gonk/ril_consts.js @@ -0,0 +1,3338 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +// Set to true to debug all RIL layers +this.DEBUG_ALL = false; + +// Set individually to debug specific layers +this.DEBUG_WORKER = false || DEBUG_ALL; +this.DEBUG_CONTENT_HELPER = false || DEBUG_ALL; +this.DEBUG_RIL = false || DEBUG_ALL; + +this.REQUEST_GET_SIM_STATUS = 1; +this.REQUEST_ENTER_SIM_PIN = 2; +this.REQUEST_ENTER_SIM_PUK = 3; +this.REQUEST_ENTER_SIM_PIN2 = 4; +this.REQUEST_ENTER_SIM_PUK2 = 5; +this.REQUEST_CHANGE_SIM_PIN = 6; +this.REQUEST_CHANGE_SIM_PIN2 = 7; +this.REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE = 8; +this.REQUEST_GET_CURRENT_CALLS = 9; +this.REQUEST_DIAL = 10; +this.REQUEST_GET_IMSI = 11; +this.REQUEST_HANGUP = 12; +this.REQUEST_HANGUP_WAITING_OR_BACKGROUND = 13; +this.REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND = 14; +this.REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE = 15; +this.REQUEST_CONFERENCE = 16; +this.REQUEST_UDUB = 17; +this.REQUEST_LAST_CALL_FAIL_CAUSE = 18; +this.REQUEST_SIGNAL_STRENGTH = 19; +this.REQUEST_VOICE_REGISTRATION_STATE = 20; +this.REQUEST_DATA_REGISTRATION_STATE = 21; +this.REQUEST_OPERATOR = 22; +this.REQUEST_RADIO_POWER = 23; +this.REQUEST_DTMF = 24; +this.REQUEST_SEND_SMS = 25; +this.REQUEST_SEND_SMS_EXPECT_MORE = 26; +this.REQUEST_SETUP_DATA_CALL = 27; +this.REQUEST_SIM_IO = 28; +this.REQUEST_SEND_USSD = 29; +this.REQUEST_CANCEL_USSD = 30; +this.REQUEST_GET_CLIR = 31; +this.REQUEST_SET_CLIR = 32; +this.REQUEST_QUERY_CALL_FORWARD_STATUS = 33; +this.REQUEST_SET_CALL_FORWARD = 34; +this.REQUEST_QUERY_CALL_WAITING = 35; +this.REQUEST_SET_CALL_WAITING = 36; +this.REQUEST_SMS_ACKNOWLEDGE = 37; +this.REQUEST_GET_IMEI = 38; +this.REQUEST_GET_IMEISV = 39; +this.REQUEST_ANSWER = 40; +this.REQUEST_DEACTIVATE_DATA_CALL = 41; +this.REQUEST_QUERY_FACILITY_LOCK = 42; +this.REQUEST_SET_FACILITY_LOCK = 43; +this.REQUEST_CHANGE_BARRING_PASSWORD = 44; +this.REQUEST_QUERY_NETWORK_SELECTION_MODE = 45; +this.REQUEST_SET_NETWORK_SELECTION_AUTOMATIC = 46; +this.REQUEST_SET_NETWORK_SELECTION_MANUAL = 47; +this.REQUEST_QUERY_AVAILABLE_NETWORKS = 48; +this.REQUEST_DTMF_START = 49; +this.REQUEST_DTMF_STOP = 50; +this.REQUEST_BASEBAND_VERSION = 51; +this.REQUEST_SEPARATE_CONNECTION = 52; +this.REQUEST_SET_MUTE = 53; +this.REQUEST_GET_MUTE = 54; +this.REQUEST_QUERY_CLIP = 55; +this.REQUEST_LAST_DATA_CALL_FAIL_CAUSE = 56; +this.REQUEST_DATA_CALL_LIST = 57; +this.REQUEST_RESET_RADIO = 58; +this.REQUEST_OEM_HOOK_RAW = 59; +this.REQUEST_OEM_HOOK_STRINGS = 60; +this.REQUEST_SCREEN_STATE = 61; +this.REQUEST_SET_SUPP_SVC_NOTIFICATION = 62; +this.REQUEST_WRITE_SMS_TO_SIM = 63; +this.REQUEST_DELETE_SMS_ON_SIM = 64; +this.REQUEST_SET_BAND_MODE = 65; +this.REQUEST_QUERY_AVAILABLE_BAND_MODE = 66; +this.REQUEST_STK_GET_PROFILE = 67; +this.REQUEST_STK_SET_PROFILE = 68; +this.REQUEST_STK_SEND_ENVELOPE_COMMAND = 69; +this.REQUEST_STK_SEND_TERMINAL_RESPONSE = 70; +this.REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM = 71; +this.REQUEST_EXPLICIT_CALL_TRANSFER = 72; +this.REQUEST_SET_PREFERRED_NETWORK_TYPE = 73; +this.REQUEST_GET_PREFERRED_NETWORK_TYPE = 74; +this.REQUEST_GET_NEIGHBORING_CELL_IDS = 75; +this.REQUEST_SET_LOCATION_UPDATES = 76; +this.REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE = 77; +this.REQUEST_CDMA_SET_ROAMING_PREFERENCE = 78; +this.REQUEST_CDMA_QUERY_ROAMING_PREFERENCE = 79; +this.REQUEST_SET_TTY_MODE = 80; +this.REQUEST_QUERY_TTY_MODE = 81; +this.REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE = 82; +this.REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE = 83; +this.REQUEST_CDMA_FLASH = 84; +this.REQUEST_CDMA_BURST_DTMF = 85; +this.REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY = 86; +this.REQUEST_CDMA_SEND_SMS = 87; +this.REQUEST_CDMA_SMS_ACKNOWLEDGE = 88; +this.REQUEST_GSM_GET_BROADCAST_SMS_CONFIG = 89; +this.REQUEST_GSM_SET_BROADCAST_SMS_CONFIG = 90; +this.REQUEST_GSM_SMS_BROADCAST_ACTIVATION = 91; +this.REQUEST_CDMA_GET_BROADCAST_SMS_CONFIG = 92; +this.REQUEST_CDMA_SET_BROADCAST_SMS_CONFIG = 93; +this.REQUEST_CDMA_SMS_BROADCAST_ACTIVATION = 94; +this.REQUEST_CDMA_SUBSCRIPTION = 95; +this.REQUEST_CDMA_WRITE_SMS_TO_RUIM = 96; +this.REQUEST_CDMA_DELETE_SMS_ON_RUIM = 97; +this.REQUEST_DEVICE_IDENTITY = 98; +this.REQUEST_EXIT_EMERGENCY_CALLBACK_MODE = 99; +this.REQUEST_GET_SMSC_ADDRESS = 100; +this.REQUEST_SET_SMSC_ADDRESS = 101; +this.REQUEST_REPORT_SMS_MEMORY_STATUS = 102; +this.REQUEST_REPORT_STK_SERVICE_IS_RUNNING = 103; +this.REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE = 104; +this.REQUEST_ISIM_AUTHENTICATION = 105; +this.REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU = 106; +this.REQUEST_STK_SEND_ENVELOPE_WITH_STATUS = 107; +this.REQUEST_VOICE_RADIO_TECH = 108; +this.REQUEST_GET_CELL_INFO_LIST = 109; +this.REQUEST_SET_UNSOL_CELL_INFO_LIST_RATE = 110; +this.REQUEST_SET_INITIAL_ATTACH_APN = 111; +this.REQUEST_IMS_REGISTRATION_STATE = 112; +this.REQUEST_IMS_SEND_SMS = 113; +this.REQUEST_SIM_TRANSMIT_APDU_BASIC = 114; +this.REQUEST_SIM_OPEN_CHANNEL = 115; +this.REQUEST_SIM_CLOSE_CHANNEL = 116; +this.REQUEST_SIM_TRANSMIT_APDU_CHANNEL = 117; +this.REQUEST_NV_READ_ITEM = 118; +this.REQUEST_NV_WRITE_ITEM = 119; +this.REQUEST_NV_WRITE_CDMA_PRL = 120; +this.REQUEST_NV_RESET_CONFIG = 121; +this.REQUEST_SET_UICC_SUBSCRIPTION = 122; +this.REQUEST_ALLOW_DATA = 123; +this.REQUEST_GET_HARDWARE_CONFIG = 124; +this.REQUEST_SIM_AUTHENTICATION = 125; +this.REQUEST_GET_DC_RT_INFO = 126; +this.REQUEST_SET_DC_RT_INFO_RATE = 127; +this.REQUEST_SET_DATA_PROFILE = 128; +this.REQUEST_SHUTDOWN = 129; + +// CAF specific parcel type. It should be synced with latest version. But CAF +// doesn't have l version for b2g yet, so we set REQUEST_SET_DATA_SUBSCRIPTION +// to a value that won't get conflict with known AOSP parcel. +this.REQUEST_SET_DATA_SUBSCRIPTION = 130; + +// Mozilla specific parcel type. +this.REQUEST_GET_UNLOCK_RETRY_COUNT = 150; + +// Fugu specific parcel types. +this.RIL_REQUEST_GPRS_ATTACH = 5018; +this.RIL_REQUEST_GPRS_DETACH = 5019; + +// Galaxy S2 specific parcel type. +this.REQUEST_DIAL_EMERGENCY_CALL = 10016; + +this.RESPONSE_TYPE_SOLICITED = 0; +this.RESPONSE_TYPE_UNSOLICITED = 1; + +this.UNSOLICITED_RESPONSE_BASE = 1000; +this.UNSOLICITED_RESPONSE_RADIO_STATE_CHANGED = 1000; +this.UNSOLICITED_RESPONSE_CALL_STATE_CHANGED = 1001; +this.UNSOLICITED_RESPONSE_VOICE_NETWORK_STATE_CHANGED = 1002; +this.UNSOLICITED_RESPONSE_NEW_SMS = 1003; +this.UNSOLICITED_RESPONSE_NEW_SMS_STATUS_REPORT = 1004; +this.UNSOLICITED_RESPONSE_NEW_SMS_ON_SIM = 1005; +this.UNSOLICITED_ON_USSD = 1006; +this.UNSOLICITED_ON_USSD_REQUEST = 1007; +this.UNSOLICITED_NITZ_TIME_RECEIVED = 1008; +this.UNSOLICITED_SIGNAL_STRENGTH = 1009; +this.UNSOLICITED_DATA_CALL_LIST_CHANGED = 1010; +this.UNSOLICITED_SUPP_SVC_NOTIFICATION = 1011; +this.UNSOLICITED_STK_SESSION_END = 1012; +this.UNSOLICITED_STK_PROACTIVE_COMMAND = 1013; +this.UNSOLICITED_STK_EVENT_NOTIFY = 1014; +this.UNSOLICITED_STK_CALL_SETUP = 1015; +this.UNSOLICITED_SIM_SMS_STORAGE_FULL = 1016; +this.UNSOLICITED_SIM_REFRESH = 1017; +this.UNSOLICITED_CALL_RING = 1018; +this.UNSOLICITED_RESPONSE_SIM_STATUS_CHANGED = 1019; +this.UNSOLICITED_RESPONSE_CDMA_NEW_SMS = 1020; +this.UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS = 1021; +this.UNSOLICITED_CDMA_RUIM_SMS_STORAGE_FULL = 1022; +this.UNSOLICITED_RESTRICTED_STATE_CHANGED = 1023; +this.UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE = 1024; +this.UNSOLICITED_CDMA_CALL_WAITING = 1025; +this.UNSOLICITED_CDMA_OTA_PROVISION_STATUS = 1026; +this.UNSOLICITED_CDMA_INFO_REC = 1027; +this.UNSOLICITED_OEM_HOOK_RAW = 1028; +this.UNSOLICITED_RINGBACK_TONE = 1029; +this.UNSOLICITED_RESEND_INCALL_MUTE = 1030; +this.UNSOLICITED_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 1031; +this.UNSOLICITED_CDMA_PRL_CHANGED = 1032; +this.UNSOLICITED_EXIT_EMERGENCY_CALLBACK_MODE = 1033; +this.UNSOLICITED_RIL_CONNECTED = 1034; +this.UNSOLICITED_VOICE_RADIO_TECH_CHANGED = 1035; +this.UNSOLICITED_CELL_INFO_LIST = 1036; +this.UNSOLICITED_RESPONSE_IMS_NETWORK_STATE_CHANGED = 1037; +this.UNSOLICITED_UICC_SUBSCRIPTION_STATUS_CHANGED = 1038; +this.UNSOLICITED_SRVCC_STATE_NOTIFY = 1039; +this.UNSOLICITED_HARDWARE_CONFIG_CHANGED = 1040; +this.UNSOLICITED_DC_RT_INFO_CHANGED = 1041; + +this.ERROR_SUCCESS = 0; +this.ERROR_RADIO_NOT_AVAILABLE = 1; +this.ERROR_GENERIC_FAILURE = 2; +this.ERROR_PASSWORD_INCORRECT = 3; +this.ERROR_SIM_PIN2 = 4; +this.ERROR_SIM_PUK2 = 5; +this.ERROR_REQUEST_NOT_SUPPORTED = 6; +this.ERROR_CANCELLED = 7; +this.ERROR_OP_NOT_ALLOWED_DURING_VOICE_CALL = 8; +this.ERROR_OP_NOT_ALLOWED_BEFORE_REG_TO_NW = 9; +this.ERROR_SMS_SEND_FAIL_RETRY = 10; +this.ERROR_SIM_ABSENT = 11; +this.ERROR_SUBSCRIPTION_NOT_AVAILABLE = 12; +this.ERROR_MODE_NOT_SUPPORTED = 13; +this.ERROR_FDN_CHECK_FAILURE = 14; +this.ERROR_ILLEGAL_SIM_OR_ME = 15; +this.ERROR_MISSING_RESOURCE = 16; +this.ERROR_NO_SUCH_ELEMENT = 17; + +this.GECKO_ERROR_RADIO_NOT_AVAILABLE = "RadioNotAvailable"; +this.GECKO_ERROR_GENERIC_FAILURE = "GenericFailure"; +this.GECKO_ERROR_PASSWORD_INCORRECT = "IncorrectPassword"; +this.GECKO_ERROR_SIM_PIN2 = "SimPin2"; +this.GECKO_ERROR_SIM_PUK2 = "SimPuk2"; +this.GECKO_ERROR_REQUEST_NOT_SUPPORTED = "RequestNotSupported"; +this.GECKO_ERROR_CANCELLED = "Cancelled"; +this.GECKO_ERROR_OP_NOT_ALLOWED_DURING_VOICE_CALL = "OpNotAllowedDuringVoiceCall"; +this.GECKO_ERROR_OP_NOT_ALLOWED_BEFORE_REG_TO_NW = "OpNotAllowedBeforeRegToNw"; +this.GECKO_ERROR_SMS_SEND_FAIL_RETRY = "SmsSendFailRetry"; +this.GECKO_ERROR_SIM_ABSENT = "SimAbsent"; +this.GECKO_ERROR_SUBSCRIPTION_NOT_AVAILABLE = "SubscriptionNotAvailable"; +this.GECKO_ERROR_MODE_NOT_SUPPORTED = "ModeNotSupported"; +this.GECKO_ERROR_FDN_CHECK_FAILURE = "FdnCheckFailure"; +this.GECKO_ERROR_ILLEGAL_SIM_OR_ME = "IllegalSIMorME"; +this.GECKO_ERROR_MISSING_RESOURCE = "MissingResource"; +this.GECKO_ERROR_NO_SUCH_ELEMENT = "NoSuchElement"; +this.GECKO_ERROR_INVALID_PARAMETER = "InvalidParameter"; +this.GECKO_ERROR_UNSPECIFIED_ERROR = "UnspecifiedError"; + +this.RIL_ERROR_TO_GECKO_ERROR = {}; +RIL_ERROR_TO_GECKO_ERROR[ERROR_RADIO_NOT_AVAILABLE] = GECKO_ERROR_RADIO_NOT_AVAILABLE; +RIL_ERROR_TO_GECKO_ERROR[ERROR_GENERIC_FAILURE] = GECKO_ERROR_GENERIC_FAILURE; +RIL_ERROR_TO_GECKO_ERROR[ERROR_PASSWORD_INCORRECT] = GECKO_ERROR_PASSWORD_INCORRECT; +RIL_ERROR_TO_GECKO_ERROR[ERROR_SIM_PIN2] = GECKO_ERROR_SIM_PIN2; +RIL_ERROR_TO_GECKO_ERROR[ERROR_SIM_PUK2] = GECKO_ERROR_SIM_PUK2; +RIL_ERROR_TO_GECKO_ERROR[ERROR_REQUEST_NOT_SUPPORTED] = GECKO_ERROR_REQUEST_NOT_SUPPORTED; +RIL_ERROR_TO_GECKO_ERROR[ERROR_CANCELLED] = GECKO_ERROR_CANCELLED; +RIL_ERROR_TO_GECKO_ERROR[ERROR_OP_NOT_ALLOWED_DURING_VOICE_CALL] = GECKO_ERROR_OP_NOT_ALLOWED_DURING_VOICE_CALL; +RIL_ERROR_TO_GECKO_ERROR[ERROR_OP_NOT_ALLOWED_BEFORE_REG_TO_NW] = GECKO_ERROR_OP_NOT_ALLOWED_BEFORE_REG_TO_NW; +RIL_ERROR_TO_GECKO_ERROR[ERROR_SMS_SEND_FAIL_RETRY] = GECKO_ERROR_SMS_SEND_FAIL_RETRY; +RIL_ERROR_TO_GECKO_ERROR[ERROR_SIM_ABSENT] = GECKO_ERROR_SIM_ABSENT; +RIL_ERROR_TO_GECKO_ERROR[ERROR_SUBSCRIPTION_NOT_AVAILABLE] = GECKO_ERROR_SUBSCRIPTION_NOT_AVAILABLE; +RIL_ERROR_TO_GECKO_ERROR[ERROR_MODE_NOT_SUPPORTED] = GECKO_ERROR_MODE_NOT_SUPPORTED; +RIL_ERROR_TO_GECKO_ERROR[ERROR_FDN_CHECK_FAILURE] = GECKO_ERROR_FDN_CHECK_FAILURE; +RIL_ERROR_TO_GECKO_ERROR[ERROR_ILLEGAL_SIM_OR_ME] = GECKO_ERROR_ILLEGAL_SIM_OR_ME; +RIL_ERROR_TO_GECKO_ERROR[ERROR_MISSING_RESOURCE] = GECKO_ERROR_MISSING_RESOURCE; +RIL_ERROR_TO_GECKO_ERROR[ERROR_NO_SUCH_ELEMENT] = GECKO_ERROR_NO_SUCH_ELEMENT; + +// 3GPP 23.040 clause 9.2.3.6 TP-Message-Reference(TP-MR): +// The number of times the MS automatically repeats the SMS-SUBMIT shall be in +// the range 1 to 3 but the precise number is an implementation matter. +this.SMS_RETRY_MAX = 3; + +this.RADIO_STATE_OFF = 0; +this.RADIO_STATE_UNAVAILABLE = 1; +this.RADIO_STATE_ON = 10; // since RIL v7 + +this.CARD_STATE_ABSENT = 0; +this.CARD_STATE_PRESENT = 1; +this.CARD_STATE_ERROR = 2; + +this.CARD_PERSOSUBSTATE_UNKNOWN = 0; +this.CARD_PERSOSUBSTATE_IN_PROGRESS = 1; +this.CARD_PERSOSUBSTATE_READY = 2; +this.CARD_PERSOSUBSTATE_SIM_NETWORK = 3; +this.CARD_PERSOSUBSTATE_SIM_NETWORK_SUBSET = 4; +this.CARD_PERSOSUBSTATE_SIM_CORPORATE = 5; +this.CARD_PERSOSUBSTATE_SIM_SERVICE_PROVIDER = 6; +this.CARD_PERSOSUBSTATE_SIM_SIM = 7; +this.CARD_PERSOSUBSTATE_SIM_NETWORK_PUK = 8; +this.CARD_PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK = 9; +this.CARD_PERSOSUBSTATE_SIM_CORPORATE_PUK = 10; +this.CARD_PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK = 11; +this.CARD_PERSOSUBSTATE_SIM_SIM_PUK = 12; +this.CARD_PERSOSUBSTATE_RUIM_NETWORK1 = 13; +this.CARD_PERSOSUBSTATE_RUIM_NETWORK2 = 14; +this.CARD_PERSOSUBSTATE_RUIM_HRPD = 15; +this.CARD_PERSOSUBSTATE_RUIM_CORPORATE = 16; +this.CARD_PERSOSUBSTATE_RUIM_SERVICE_PROVIDER = 17; +this.CARD_PERSOSUBSTATE_RUIM_RUIM = 18; +this.CARD_PERSOSUBSTATE_RUIM_NETWORK1_PUK = 19; +this.CARD_PERSOSUBSTATE_RUIM_NETWORK2_PUK = 20; +this.CARD_PERSOSUBSTATE_RUIM_HRPD_PUK = 21; +this.CARD_PERSOSUBSTATE_RUIM_CORPORATE_PUK = 22; +this.CARD_PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK = 23; +this.CARD_PERSOSUBSTATE_RUIM_RUIM_PUK = 24; + +this.CARD_APPSTATE_ILLEGAL = -1; +this.CARD_APPSTATE_UNKNOWN = 0; +this.CARD_APPSTATE_DETECTED = 1; +this.CARD_APPSTATE_PIN = 2; // If PIN1 or UPin is required. +this.CARD_APPSTATE_PUK = 3; // If PUK1 or Puk for UPin is required. +this.CARD_APPSTATE_SUBSCRIPTION_PERSO = 4; // perso_substate should be looked + // at when app_state is assigned + // to this value. +this.CARD_APPSTATE_READY = 5; + +this.CARD_PINSTATE_UNKNOWN = 0; +this.CARD_PINSTATE_ENABLED_NOT_VERIFIED = 1; +this.CARD_PINSTATE_ENABLED_VERIFIED = 2; +this.CARD_PINSTATE_DISABLED = 3; +this.CARD_PINSTATE_ENABLED_BLOCKED = 4; +this.CARD_PINSTATE_ENABLED_PERM_BLOCKED = 5; + +this.CARD_APPTYPE_UNKNOWN = 0; +this.CARD_APPTYPE_SIM = 1; +this.CARD_APPTYPE_USIM = 2; +this.CARD_APPTYPE_RUIM = 3; +this.CARD_APPTYPE_CSIM = 4; +this.CARD_APPTYPE_ISIM = 5; + +this.CARD_MAX_APPS = 8; + +this.GECKO_CARD_TYPE = [ + null, + "sim", + "usim", + "ruim", + "csim", + "isim" +]; + + +// Used for QUERY_AVAILABLE_NETWORKS status. +this.QAN_STATE_UNKNOWN = "unknown"; +this.QAN_STATE_AVAILABLE = "available"; +this.QAN_STATE_CURRENT = "current"; +this.QAN_STATE_FORBIDDEN = "forbidden"; + +// Must be in sync with MobileNetworkState of MozMobileNetworkInfo.webidl +this.GECKO_QAN_STATE_UNKNOWN = null; +this.GECKO_QAN_STATE_AVAILABLE = "available"; +this.GECKO_QAN_STATE_CONNECTED = "connected"; +this.GECKO_QAN_STATE_FORBIDDEN = "forbidden"; + +this.RIL_QAN_STATE_TO_GECKO_STATE = {}; +this.RIL_QAN_STATE_TO_GECKO_STATE[this.QAN_STATE_UNKNOWN] = this.GECKO_QAN_STATE_UNKNOWN; +this.RIL_QAN_STATE_TO_GECKO_STATE[this.QAN_STATE_AVAILABLE] = this.GECKO_QAN_STATE_AVAILABLE; +this.RIL_QAN_STATE_TO_GECKO_STATE[this.QAN_STATE_CURRENT] = this.GECKO_QAN_STATE_CONNECTED; +this.RIL_QAN_STATE_TO_GECKO_STATE[this.QAN_STATE_FORBIDDEN] = this.GECKO_QAN_STATE_FORBIDDEN; + +this.NETWORK_SELECTION_MODE_AUTOMATIC = 0; +this.NETWORK_SELECTION_MODE_MANUAL = 1; + +this.NETWORK_INFO_VOICE_REGISTRATION_STATE = "voiceRegistrationState"; +this.NETWORK_INFO_DATA_REGISTRATION_STATE = "dataRegistrationState"; +this.NETWORK_INFO_OPERATOR = "operator"; +this.NETWORK_INFO_NETWORK_SELECTION_MODE = "networkSelectionMode"; +this.NETWORK_INFO_SIGNAL = "signal"; +this.NETWORK_INFO_MESSAGE_TYPES = [ + NETWORK_INFO_VOICE_REGISTRATION_STATE, + NETWORK_INFO_DATA_REGISTRATION_STATE, + NETWORK_INFO_OPERATOR, + NETWORK_INFO_NETWORK_SELECTION_MODE, + NETWORK_INFO_SIGNAL +]; + +this.GECKO_PREFERRED_NETWORK_TYPE_WCDMA_GSM = "wcdma/gsm"; +this.GECKO_PREFERRED_NETWORK_TYPE_GSM_ONLY = "gsm"; +this.GECKO_PREFERRED_NETWORK_TYPE_WCDMA_ONLY = "wcdma"; +this.GECKO_PREFERRED_NETWORK_TYPE_WCDMA_GSM_AUTO = "wcdma/gsm-auto"; +this.GECKO_PREFERRED_NETWORK_TYPE_CDMA_EVDO = "cdma/evdo"; +this.GECKO_PREFERRED_NETWORK_TYPE_CDMA_ONLY = "cdma"; +this.GECKO_PREFERRED_NETWORK_TYPE_EVDO_ONLY = "evdo"; +this.GECKO_PREFERRED_NETWORK_TYPE_WCDMA_GSM_CDMA_EVDO = "wcdma/gsm/cdma/evdo"; +this.GECKO_PREFERRED_NETWORK_TYPE_LTE_CDMA_EVDO = "lte/cdma/evdo"; +this.GECKO_PREFERRED_NETWORK_TYPE_LTE_WCDMA_GSM = "lte/wcdma/gsm"; +this.GECKO_PREFERRED_NETWORK_TYPE_LTE_WCDMA = "lte/wcdma"; +this.GECKO_PREFERRED_NETWORK_TYPE_LTE_WCDMA_GSM_CDMA_EVDO = "lte/wcdma/gsm/cdma/evdo"; +this.GECKO_PREFERRED_NETWORK_TYPE_LTE_ONLY = "lte"; +this.RIL_PREFERRED_NETWORK_TYPE_TO_GECKO = [ + GECKO_PREFERRED_NETWORK_TYPE_WCDMA_GSM, + GECKO_PREFERRED_NETWORK_TYPE_GSM_ONLY, + GECKO_PREFERRED_NETWORK_TYPE_WCDMA_ONLY, + GECKO_PREFERRED_NETWORK_TYPE_WCDMA_GSM_AUTO, + GECKO_PREFERRED_NETWORK_TYPE_CDMA_EVDO, + GECKO_PREFERRED_NETWORK_TYPE_CDMA_ONLY, + GECKO_PREFERRED_NETWORK_TYPE_EVDO_ONLY, + GECKO_PREFERRED_NETWORK_TYPE_WCDMA_GSM_CDMA_EVDO, + GECKO_PREFERRED_NETWORK_TYPE_LTE_CDMA_EVDO, + GECKO_PREFERRED_NETWORK_TYPE_LTE_WCDMA_GSM, + GECKO_PREFERRED_NETWORK_TYPE_LTE_WCDMA_GSM_CDMA_EVDO, + GECKO_PREFERRED_NETWORK_TYPE_LTE_ONLY, + GECKO_PREFERRED_NETWORK_TYPE_LTE_WCDMA +]; + +this.GECKO_SUPPORTED_NETWORK_TYPES_DEFAULT = "gsm,wcdma"; +// Index-item pair must be in sync with nsIMobileConnection.MOBILE_NETWORK_TYPE_* +this.GECKO_SUPPORTED_NETWORK_TYPES = [ + "gsm", + "wcdma", + "cdma", + "evdo", + "lte" +]; + +// Network registration states. See TS 27.007 7.2 +this.NETWORK_CREG_STATE_NOT_SEARCHING = 0; +this.NETWORK_CREG_STATE_REGISTERED_HOME = 1; +this.NETWORK_CREG_STATE_SEARCHING = 2; +this.NETWORK_CREG_STATE_DENIED = 3; +this.NETWORK_CREG_STATE_UNKNOWN = 4; +this.NETWORK_CREG_STATE_REGISTERED_ROAMING = 5; +this.NETWORK_CREG_STATE_NOT_SEARCHING_EMERGENCY_CALLS = 10; +this.NETWORK_CREG_STATE_SEARCHING_EMERGENCY_CALLS = 12; +this.NETWORK_CREG_STATE_DENIED_EMERGENCY_CALLS = 13; +this.NETWORK_CREG_STATE_UNKNOWN_EMERGENCY_CALLS = 14; + +this.NETWORK_CREG_TECH_UNKNOWN = 0; +this.NETWORK_CREG_TECH_GPRS = 1; +this.NETWORK_CREG_TECH_EDGE = 2; +this.NETWORK_CREG_TECH_UMTS = 3; +this.NETWORK_CREG_TECH_IS95A = 4; +this.NETWORK_CREG_TECH_IS95B = 5; +this.NETWORK_CREG_TECH_1XRTT = 6; +this.NETWORK_CREG_TECH_EVDO0 = 7; +this.NETWORK_CREG_TECH_EVDOA = 8; +this.NETWORK_CREG_TECH_HSDPA = 9; +this.NETWORK_CREG_TECH_HSUPA = 10; +this.NETWORK_CREG_TECH_HSPA = 11; +this.NETWORK_CREG_TECH_EVDOB = 12; +this.NETWORK_CREG_TECH_EHRPD = 13; +this.NETWORK_CREG_TECH_LTE = 14; +this.NETWORK_CREG_TECH_HSPAP = 15; +this.NETWORK_CREG_TECH_GSM = 16; +this.NETWORK_CREG_TECH_DCHSPAP_1 = 18; // Some devices reports as 18 +this.NETWORK_CREG_TECH_DCHSPAP_2 = 19; // Some others report it as 19 + +this.CELL_INFO_TYPE_GSM = 1; +this.CELL_INFO_TYPE_CDMA = 2; +this.CELL_INFO_TYPE_LTE = 3; +this.CELL_INFO_TYPE_WCDMA = 4; + +this.CALL_STATE_UNKNOWN = -1; +this.CALL_STATE_ACTIVE = 0; +this.CALL_STATE_HOLDING = 1; +this.CALL_STATE_DIALING = 2; +this.CALL_STATE_ALERTING = 3; +this.CALL_STATE_INCOMING = 4; +this.CALL_STATE_WAITING = 5; + +this.TOA_INTERNATIONAL = 0x91; +this.TOA_UNKNOWN = 0x81; + +this.CALL_PRESENTATION_ALLOWED = 0; +this.CALL_PRESENTATION_RESTRICTED = 1; +this.CALL_PRESENTATION_UNKNOWN = 2; +this.CALL_PRESENTATION_PAYPHONE = 3; + +// Call forwarding actions, see TS 27.007 7.11 "mode" +this.CALL_FORWARD_ACTION_QUERY_STATUS = 2; + +// ICC commands, see TS 27.007 +CRSM commands +this.ICC_COMMAND_SEEK = 0xa2; +this.ICC_COMMAND_READ_BINARY = 0xb0; +this.ICC_COMMAND_READ_RECORD = 0xb2; +this.ICC_COMMAND_GET_RESPONSE = 0xc0; +this.ICC_COMMAND_UPDATE_BINARY = 0xd6; +this.ICC_COMMAND_UPDATE_RECORD = 0xdc; + +// ICC constants, GSM SIM file ids from TS 51.011 +this.ICC_EF_ICCID = 0x2fe2; +this.ICC_EF_IMG = 0x4f20; +this.ICC_EF_PBR = 0x4f30; +this.ICC_EF_PLMNsel = 0x6f30; // PLMN for SIM +this.ICC_EF_SST = 0x6f38; +this.ICC_EF_UST = 0x6f38; // For USIM +this.ICC_EF_ADN = 0x6f3a; +this.ICC_EF_FDN = 0x6f3b; +this.ICC_EF_SMS = 0x6f3c; +this.ICC_EF_GID1 = 0x6f3e; +this.ICC_EF_MSISDN = 0x6f40; +this.ICC_EF_CBMI = 0x6f45; +this.ICC_EF_SPN = 0x6f46; +this.ICC_EF_CBMID = 0x6f48; +this.ICC_EF_SDN = 0x6f49; +this.ICC_EF_EXT1 = 0x6f4a; +this.ICC_EF_EXT2 = 0x6f4b; +this.ICC_EF_EXT3 = 0x6f4c; +this.ICC_EF_CBMIR = 0x6f50; +this.ICC_EF_AD = 0x6fad; +this.ICC_EF_PHASE = 0x6fae; +this.ICC_EF_PNN = 0x6fc5; +this.ICC_EF_OPL = 0x6fc6; +this.ICC_EF_MBDN = 0x6fc7; +this.ICC_EF_EXT6 = 0x6fc8; // Ext record for EF[MBDN] +this.ICC_EF_MBI = 0x6fc9; +this.ICC_EF_MWIS = 0x6fca; +this.ICC_EF_CFIS = 0x6fcb; +this.ICC_EF_SPDI = 0x6fcd; + +// CPHS files to be supported +this.ICC_EF_CPHS_INFO = 0x6f16; // CPHS Information +this.ICC_EF_CPHS_MBN = 0x6f17; // Mailbox Numbers + +// CSIM files +this.ICC_EF_CSIM_IMSI_M = 0x6f22; +this.ICC_EF_CSIM_CDMAHOME = 0x6f28; +this.ICC_EF_CSIM_CST = 0x6f32; // CDMA Service table +this.ICC_EF_CSIM_SPN = 0x6f41; + +this.ICC_PHASE_1 = 0x00; +this.ICC_PHASE_2 = 0x02; +this.ICC_PHASE_2_PROFILE_DOWNLOAD_REQUIRED = 0x03; + +// Types of files TS 11.11 9.3 +this.TYPE_RFU = 0; +this.TYPE_MF = 1; +this.TYPE_DF = 2; +this.TYPE_EF = 4; + +this.RESPONSE_DATA_FILE_SIZE = 2; +this.RESPONSE_DATA_FILE_ID_1 = 4; +this.RESPONSE_DATA_FILE_ID_2 = 5; +this.RESPONSE_DATA_FILE_TYPE = 6; +this.RESPONSE_DATA_RFU_3 = 7; +this.RESPONSE_DATA_ACCESS_CONDITION_1 = 8; +this.RESPONSE_DATA_ACCESS_CONDITION_2 = 9; +this.RESPONSE_DATA_ACCESS_CONDITION_3 = 10; +this.RESPONSE_DATA_FILE_STATUS = 11; +this.RESPONSE_DATA_LENGTH = 12; +this.RESPONSE_DATA_STRUCTURE = 13; +this.RESPONSE_DATA_RECORD_LENGTH = 14; + +// Structure of files TS 11.11 9.3 +this.EF_STRUCTURE_TRANSPARENT = 0; +this.EF_STRUCTURE_LINEAR_FIXED = 1; +this.EF_STRUCTURE_CYCLIC = 3; + +// TS 102.221 11.1.1.4.3 Table 11.5: File descriptor byte. +this.UICC_EF_STRUCTURE = {}; +this.UICC_EF_STRUCTURE[this.EF_STRUCTURE_TRANSPARENT]= 1; +this.UICC_EF_STRUCTURE[this.EF_STRUCTURE_LINEAR_FIXED]= 2; +this.UICC_EF_STRUCTURE[this.EF_STRUCTURE_CYCLIC]= 6; + +// Status code of EFsms +// see 3GPP TS 51.011 clause 10.5.3 +this.EFSMS_STATUS_FREE = 0x00; +this.EFSMS_STATUS_READ = 0x01; +this.EFSMS_STATUS_TO_BE_READ = 0x03; +this.EFSMS_STATUS_TO_BE_SENT = 0x07; + +// Total size of ADN footer(the size of Alpha identifier excluded). +// See TS 151.011 clause 10.5.1 EF_ADN. +this.ADN_FOOTER_SIZE_BYTES = 14; +// Maximum size of BCD numbers in ADN. +// See TS 151.011 clause 10.5.1 EF_ADN, 'Length of BCD number/SSC contents'. +this.ADN_MAX_BCD_NUMBER_BYTES = 11; +// Maximum digits of the Dialling Number in ADN. +// See TS 151.011 clause 10.5.1 EF_ADN, 'Dialling Number'. +this.ADN_MAX_NUMBER_DIGITS = 20; +// Maximum size of BCD numbers in EXT. +// See TS 151.011 clause 10.5.10 EF_EXT1, 'Extension data'. +this.EXT_MAX_BCD_NUMBER_BYTES = 10; +// Maximum digits of the Dialling Number in EXT. +// See TS 151.011 clause 10.5.10 EF_EXT1, 'Extension data'. +this.EXT_MAX_NUMBER_DIGITS = 20; + +// READ_RECORD mode, TS 102.221 +this.READ_RECORD_ABSOLUTE_MODE = 4; + +// TS 102.221 Table 11.2, return FCP template +this.GET_RESPONSE_FCP_TEMPLATE = 4; + +// GET_RESPONSE mandatory response size for EF, see TS 51.011 clause 9, +// 'Response data in case of an EF.' +this.GET_RESPONSE_EF_SIZE_BYTES = 15; + +// EF path +this.EF_PATH_MF_SIM = "3f00"; +this.EF_PATH_DF_PHONEBOOK = "5f3a"; +this.EF_PATH_GRAPHICS = "5f50"; +this.EF_PATH_DF_TELECOM = "7f10"; +this.EF_PATH_DF_GSM = "7f20"; +this.EF_PATH_DF_CDMA = "7f25"; +this.EF_PATH_ADF_USIM = "7fff"; + +// Status code of sw1 for ICC I/O, +// see GSM11.11 and TS 51.011 clause 9.4, and ISO 7816-4 +this.ICC_STATUS_NORMAL_ENDING = 0x90; +this.ICC_STATUS_NORMAL_ENDING_WITH_EXTRA = 0x91; +this.ICC_STATUS_SAT_BUSY = 0x93; +this.ICC_STATUS_WITH_SIM_DATA = 0x9e; +this.ICC_STATUS_WITH_RESPONSE_DATA = 0x9f; +this.ICC_STATUS_ERROR_WRONG_LENGTH = 0x67; +this.ICC_STATUS_ERROR_COMMAND_NOT_ALLOWED = 0x69; +this.ICC_STATUS_ERROR_WRONG_PARAMETERS = 0x6a; + +// ICC call barring facility. +// TS 27.007, clause 7.4, +CLCK +this.ICC_CB_FACILITY_SIM = "SC"; +this.ICC_CB_FACILITY_FDN = "FD"; +this.ICC_CB_FACILITY_BAOC = "AO"; +this.ICC_CB_FACILITY_BAOIC = "OI"; +this.ICC_CB_FACILITY_BAOICxH = "OX"; +this.ICC_CB_FACILITY_BAIC = "AI"; +this.ICC_CB_FACILITY_BAICr = "IR"; +this.ICC_CB_FACILITY_BA_ALL = "AB"; +this.ICC_CB_FACILITY_BA_MO = "AG"; +this.ICC_CB_FACILITY_BA_MT = "AC"; + +// ICC service class +// TS 27.007, clause 7.4, +CLCK +this.ICC_SERVICE_CLASS_NONE = 0; // no user input +this.ICC_SERVICE_CLASS_VOICE = (1 << 0); +this.ICC_SERVICE_CLASS_DATA = (1 << 1); +this.ICC_SERVICE_CLASS_FAX = (1 << 2); +this.ICC_SERVICE_CLASS_SMS = (1 << 3); +this.ICC_SERVICE_CLASS_DATA_SYNC = (1 << 4); +this.ICC_SERVICE_CLASS_DATA_ASYNC = (1 << 5); +this.ICC_SERVICE_CLASS_PACKET = (1 << 6); +this.ICC_SERVICE_CLASS_PAD = (1 << 7); +this.ICC_SERVICE_CLASS_MAX = (1 << 7); // Max ICC_SERVICE_CLASS value + +// ICC lock-selection codes +// TS 27.007, clause 8.65, +CPINR +this.ICC_SEL_CODE_SIM_PIN = "SIM PIN"; +this.ICC_SEL_CODE_SIM_PUK = "SIM PUK"; +this.ICC_SEL_CODE_PH_SIM_PIN = "PH-SIM PIN"; +this.ICC_SEL_CODE_PH_FSIM_PIN = "PH-FSIM PIN"; +this.ICC_SEL_CODE_PH_FSIM_PUK = "PH-FSIM PUK"; +this.ICC_SEL_CODE_SIM_PIN2 = "SIM PIN2"; +this.ICC_SEL_CODE_SIM_PUK2 = "SIM PUK2"; +this.ICC_SEL_CODE_PH_NET_PIN = "PH-NET PIN"; +this.ICC_SEL_CODE_PH_NET_PUK = "PH-NET PUK"; +this.ICC_SEL_CODE_PH_NETSUB_PIN = "PH-NETSUB PIN"; +this.ICC_SEL_CODE_PH_NETSUB_PUK = "PH-NETSUB PUK"; +this.ICC_SEL_CODE_PH_SP_PIN = "PH-SP PIN"; +this.ICC_SEL_CODE_PH_SP_PUK = "PH-SP PUK"; +this.ICC_SEL_CODE_PH_CORP_PIN = "PH-CORP PIN"; +this.ICC_SEL_CODE_PH_CORP_PUK = "PH-CORP PUK"; +// TODO: Bug 1116072: identify the mapping between RIL_PERSOSUBSTATE_SIM_SIM @ +// ril.h and TS 27.007, clause 8.65 for GECKO_CARDLOCK_PCK. + +this.ICC_USIM_TYPE1_TAG = 0xa8; +this.ICC_USIM_TYPE2_TAG = 0xa9; +this.ICC_USIM_TYPE3_TAG = 0xaa; +this.ICC_USIM_EFADN_TAG = 0xc0; +this.ICC_USIM_EFIAP_TAG = 0xc1; +this.ICC_USIM_EFEXT1_TAG = 0xc2; +this.ICC_USIM_EFSNE_TAG = 0xc3; +this.ICC_USIM_EFANR_TAG = 0xc4; +this.ICC_USIM_EFPBC_TAG = 0xc5; +this.ICC_USIM_EFGRP_TAG = 0xc6; +this.ICC_USIM_EFAAS_TAG = 0xc7; +this.ICC_USIM_EFGSD_TAG = 0xc8; +this.ICC_USIM_EFUID_TAG = 0xc9; +this.ICC_USIM_EFEMAIL_TAG = 0xca; +this.ICC_USIM_EFCCP1_TAG = 0xcb; + +// ICC image coding scheme +// TS 31.102, sub-clause 4.6.1.1 +this.ICC_IMG_CODING_SCHEME_BASIC = 0x11; +this.ICC_IMG_CODING_SCHEME_COLOR = 0x21; +this.ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY = 0x22; + +// Must be in sync with enum IccImageCodingScheme in MozStkCommandEvent.webidl. +this.GECKO_IMG_CODING_SCHEME_BASIC = "basic"; +this.GECKO_IMG_CODING_SCHEME_COLOR = "color"; +this.GECKO_IMG_CODING_SCHEME_COLOR_TRANSPARENCY = "color-transparency"; + +this.ICC_IMG_CODING_SCHEME_TO_GECKO = {}; +ICC_IMG_CODING_SCHEME_TO_GECKO[ICC_IMG_CODING_SCHEME_BASIC] = GECKO_IMG_CODING_SCHEME_BASIC; +ICC_IMG_CODING_SCHEME_TO_GECKO[ICC_IMG_CODING_SCHEME_COLOR] = GECKO_IMG_CODING_SCHEME_COLOR; +ICC_IMG_CODING_SCHEME_TO_GECKO[ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY] = GECKO_IMG_CODING_SCHEME_COLOR_TRANSPARENCY; + +// ICC image header size per coding scheme +// TS 31.102, Annex B +this.ICC_IMG_HEADER_SIZE_BASIC = 2; +this.ICC_IMG_HEADER_SIZE_COLOR = 6; + +this.ICC_CLUT_ENTRY_SIZE = 3; + +this.USIM_PBR_ANR = "anr"; +this.USIM_PBR_ANR0 = "anr0"; +this.USIM_PBR_EMAIL = "email"; + +// Current supported fields. Adding more fields to read will increasing I/O +// time dramatically, do check the performance is acceptable when you add +// more fields. +this.USIM_PBR_FIELDS = [USIM_PBR_EMAIL, USIM_PBR_ANR0]; + +this.USIM_TAG_NAME = {}; +this.USIM_TAG_NAME[ICC_USIM_EFADN_TAG] = "adn"; +this.USIM_TAG_NAME[ICC_USIM_EFIAP_TAG] ="iap"; +this.USIM_TAG_NAME[ICC_USIM_EFEXT1_TAG] = "ext1"; +this.USIM_TAG_NAME[ICC_USIM_EFSNE_TAG] = "sne"; +this.USIM_TAG_NAME[ICC_USIM_EFANR_TAG] = "anr"; +this.USIM_TAG_NAME[ICC_USIM_EFPBC_TAG] = "pbc"; +this.USIM_TAG_NAME[ICC_USIM_EFGRP_TAG] = "grp"; +this.USIM_TAG_NAME[ICC_USIM_EFAAS_TAG] = "aas"; +this.USIM_TAG_NAME[ICC_USIM_EFGSD_TAG] = "gsd"; +this.USIM_TAG_NAME[ICC_USIM_EFUID_TAG] = "uid"; +this.USIM_TAG_NAME[ICC_USIM_EFEMAIL_TAG] = "email"; +this.USIM_TAG_NAME[ICC_USIM_EFCCP1_TAG] = "ccp1"; + +// Error message for ICC contact. +this.CONTACT_ERR_REQUEST_NOT_SUPPORTED = GECKO_ERROR_REQUEST_NOT_SUPPORTED; +this.CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED = "ContactTypeNotSupported"; +this.CONTACT_ERR_FIELD_NOT_SUPPORTED = "FieldNotSupported"; +this.CONTACT_ERR_NO_FREE_RECORD_FOUND = "NoFreeRecordFound"; +this.CONTACT_ERR_CANNOT_ACCESS_PHONEBOOK = "CannotAccessPhoneBook"; + +// CDMA IMSI_M's byte const. +// 3GPP2 C.S0065 Sec. 5.2.2 +this.CSIM_IMSI_M_MIN2_BYTE = 1; +this.CSIM_IMSI_M_MIN1_BYTE = 3; +this.CSIM_IMSI_M_MNC_BYTE = 6; +this.CSIM_IMSI_M_PROGRAMMED_BYTE = 7; +this.CSIM_IMSI_M_MCC_BYTE = 8; + +/** + * Tags for Ber Tlv. + * See 3GPP TS 101 220 clause 7.2 - Assigned TLV tag values. + */ +this.BER_UNKNOWN_TAG = 0x00; +this.BER_FCP_TEMPLATE_TAG = 0x62; +this.BER_FCP_FILE_SIZE_DATA_TAG = 0x80; +this.BER_FCP_FILE_SIZE_TOTAL_TAG = 0x81; +this.BER_FCP_FILE_DESCRIPTOR_TAG = 0x82; +this.BER_FCP_FILE_IDENTIFIER_TAG = 0x83; +this.BER_FCP_DF_NAME_TAG = 0x84; // AID. +this.BER_FCP_PROPRIETARY_PRIMITIVE_TAG = 0x85; +this.BER_FCP_SFI_SUPPORT_TAG = 0x88; +this.BER_FCP_LIFE_CYCLE_STATUS_TAG = 0x8a; +this.BER_FCP_SA_REFERENCE_FORMAT_TAG = 0x8b; // Security Attribute - Reference Format. +this.BER_FCP_SA_COMPACT_FORMAT_TAG = 0x8c; // Security Attribute - Compact Format. +this.BER_FCP_SAT_EXPANDED_FORMAT_TAG = 0xab; // Security Attribute Template - Expanded Format. +this.BER_FCP_PROPRIETARY_TEMPLATE_TAG = 0xa5; +this.BER_FCP_PIN_STATUS_DATA_OBJECTS_TAG = 0xc6; +this.BER_PROACTIVE_COMMAND_TAG = 0xd0; +this.BER_SMS_PP_DOWNLOAD_TAG = 0xd1; +this.BER_MENU_SELECTION_TAG = 0xd3; +this.BER_EVENT_DOWNLOAD_TAG = 0xd6; +this.BER_TIMER_EXPIRATION_TAG = 0xd7; + +// Flags in Comprehension TLV. +this.COMPREHENSIONTLV_FLAG_CR = 0x80; // Comprehension required. + +// Tags for Comprehension TLV. +this.COMPREHENSIONTLV_TAG_COMMAND_DETAILS = 0x01; +this.COMPREHENSIONTLV_TAG_DEVICE_ID = 0x02; +this.COMPREHENSIONTLV_TAG_RESULT = 0x03; +this.COMPREHENSIONTLV_TAG_DURATION = 0x04; +this.COMPREHENSIONTLV_TAG_ALPHA_ID = 0x05; +this.COMPREHENSIONTLV_TAG_ADDRESS = 0x06; +this.COMPREHENSIONTLV_TAG_SUBADDRESS = 0x08; +this.COMPREHENSIONTLV_TAG_SMS_TPDU = 0x0b; +this.COMPREHENSIONTLV_TAG_TEXT_STRING = 0x0d; +this.COMPREHENSIONTLV_TAG_TONE = 0x0e; +this.COMPREHENSIONTLV_TAG_ITEM = 0x0f; +this.COMPREHENSIONTLV_TAG_ITEM_ID = 0x10; +this.COMPREHENSIONTLV_TAG_RESPONSE_LENGTH = 0x11; +this.COMPREHENSIONTLV_TAG_FILE_LIST = 0x12; +this.COMPREHENSIONTLV_TAG_LOCATION_INFO = 0x13; +this.COMPREHENSIONTLV_TAG_IMEI = 0x14; +this.COMPREHENSIONTLV_TAG_HELP_REQUEST = 0x15; +this.COMPREHENSIONTLV_TAG_NMR = 0x16; +this.COMPREHENSIONTLV_TAG_DEFAULT_TEXT = 0x17; +this.COMPREHENSIONTLV_TAG_NEXT_ACTION_IND = 0x18; +this.COMPREHENSIONTLV_TAG_CAUSE = 0x1a; +this.COMPREHENSIONTLV_TAG_LOCATION_STATUS = 0x1b; +this.COMPREHENSIONTLV_TAG_TRANSACTION_ID = 0x1c; +this.COMPREHENSIONTLV_TAG_EVENT_LIST = 0x19; +this.COMPREHENSIONTLV_TAG_ICON_ID = 0x1e; +this.COMPREHENSIONTLV_TAG_ICON_ID_LIST = 0x1f; +this.COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER = 0x24; +this.COMPREHENSIONTLV_TAG_TIMER_VALUE = 0x25; +this.COMPREHENSIONTLV_TAG_DATE_TIME_ZONE = 0x26; +this.COMPREHENSIONTLV_TAG_IMMEDIATE_RESPONSE = 0x2b; +this.COMPREHENSIONTLV_TAG_LANGUAGE = 0x2d; +this.COMPREHENSIONTLV_TAG_URL = 0x31; +this.COMPREHENSIONTLV_TAG_BROWSER_TERMINATION_CAUSE = 0x34; +this.COMPREHENSIONTLV_TAG_ACCESS_TECH = 0x3f; +this.COMPREHENSIONTLV_TAG_SERVICE_RECORD = 0x41; +this.COMPREHENSIONTLV_TAG_IMEISV = 0x62; +this.COMPREHENSIONTLV_TAG_BATTERY_STATE = 0x63; +this.COMPREHENSIONTLV_TAG_NETWORK_SEARCH_MODE = 0x65; +this.COMPREHENSIONTLV_TAG_MEID = 0x6d; +this.COMPREHENSIONTLV_TAG_BROADCAST_NETWORK_INFO = 0x7a; + +// Tags for Service Provider Display Information TLV +this.SPDI_TAG_SPDI = 0xa3; +this.SPDI_TAG_PLMN_LIST = 0x80; + +// MM INFORMATION message content IEIs +// See 3GPP TS 24.008 table 9.2.18 +this.PNN_IEI_FULL_NETWORK_NAME = 0x43; +this.PNN_IEI_SHORT_NETWORK_NAME = 0x45; + +// Device identifiers, see TS 11.14, clause 12.7 +this.STK_DEVICE_ID_KEYPAD = 0x01; +this.STK_DEVICE_ID_DISPLAY = 0x02; +this.STK_DEVICE_ID_EARPIECE = 0x03; +this.STK_DEVICE_ID_SIM = 0x81; +this.STK_DEVICE_ID_ME = 0x82; +this.STK_DEVICE_ID_NETWORK = 0x83; + +// STK Proactive commands. +this.STK_CMD_REFRESH = 0x01; +this.STK_CMD_MORE_TIME = 0x02; +this.STK_CMD_POLL_INTERVAL = 0x03; +this.STK_CMD_POLL_OFF = 0x04; +this.STK_CMD_SET_UP_EVENT_LIST = 0x05; +this.STK_CMD_SET_UP_CALL = 0x10; +this.STK_CMD_SEND_SS = 0x11; +this.STK_CMD_SEND_USSD = 0x12; +this.STK_CMD_SEND_SMS = 0x13; +this.STK_CMD_SEND_DTMF = 0x14; +this.STK_CMD_LAUNCH_BROWSER = 0x15; +this.STK_CMD_PLAY_TONE = 0x20; +this.STK_CMD_DISPLAY_TEXT = 0x21; +this.STK_CMD_GET_INKEY = 0x22; +this.STK_CMD_GET_INPUT = 0x23; +this.STK_CMD_SELECT_ITEM = 0x24; +this.STK_CMD_SET_UP_MENU = 0x25; +this.STK_CMD_PROVIDE_LOCAL_INFO = 0x26; +this.STK_CMD_TIMER_MANAGEMENT = 0x27; +this.STK_CMD_SET_UP_IDLE_MODE_TEXT = 0x28; +this.STK_CMD_OPEN_CHANNEL = 0x40; +this.STK_CMD_CLOSE_CHANNEL = 0x41; +this.STK_CMD_RECEIVE_DATA = 0x42; +this.STK_CMD_SEND_DATA = 0x43; + +// STK Result code. +// TS 11.14, clause 12.12 + +// Results '0X' and '1X' indicate that the command has been performed. + +// Command performed successfully. +this.STK_RESULT_OK = 0x00; + +// Command performed with partial comprehension. +this.STK_RESULT_PRFRMD_WITH_PARTIAL_COMPREHENSION = 0x01; + +// Command performed, with missing information. +this.STK_RESULT_PRFRMD_WITH_MISSING_INFO = 0x02; + +// REFRESH performed with additional EFs read. +this.STK_RESULT_PRFRMD_WITH_ADDITIONAL_EFS_READ = 0x03; + +// Command performed successfully, but requested icon could not be +// displayed. +this.STK_RESULT_PRFRMD_ICON_NOT_DISPLAYED = 0x04; + +// Command performed, but modified by call control by NAA. +this.STK_RESULT_PRFRMD_MODIFIED_BY_NAA = 0x05; + +// Command performed successfully, limited service. +this.STK_RESULT_PRFRMD_LIMITED_SERVICE = 0x06; + +// Command performed with modification. +this.STK_RESULT_PRFRMD_WITH_MODIFICATION = 0x07; + +// REFRESH performed but indicated NAA was not active. +this.STK_RESULT_PRFRMD_NAA_NOT_ACTIVE = 0x08; + +// Command performed successfully; tone not played. +this.STK_RESULT_PRFRMD_TONE_NOT_PLAYED = 0x09; + +// Proactive UICC session terminated by the user. +this.STK_RESULT_UICC_SESSION_TERM_BY_USER = 0x10; + +// Backward move in the proactive UICC session requested by the user. +this.STK_RESULT_BACKWARD_MOVE_BY_USER = 0x11; + +// No response from user. +this.STK_RESULT_NO_RESPONSE_FROM_USER = 0x12; + +// Help information required by the user. +this.STK_RESULT_HELP_INFO_REQUIRED = 0x13; + +// USSD or SS transaction terminated by the user. +this.STK_RESULT_USSD_SS_SESSION_TERM_BY_USER = 0x14; + +// Results '2X' indicate to the UICC that it may be worth re-trying the +// command at a later opportunity. + +// Terminal currently unable to process command. +this.STK_RESULT_TERMINAL_CRNTLY_UNABLE_TO_PROCESS = 0x20; + +// Network currently unable to process command. +this.STK_RESULT_NETWORK_CRNTLY_UNABLE_TO_PROCESS = 0x21; + +// User did not accept the proactive command. +this.STK_RESULT_USER_NOT_ACCEPT = 0x22; + +// User cleared down call before connection or network release. +this.STK_RESULT_USER_CLEAR_DOWN_CALL = 0x23; + +// Action in contradiction with the current timer state. +this.STK_RESULT_CONTRADICTION_WITH_TIMER = 0x24; + +// Interaction with call control by NAA; temporary problem. +this.STK_RESULT_NAA_CALL_CONTROL_TEMPORARY = 0x25; + +// Launch browser generic error code. +this.STK_RESULT_LAUNCH_BROWSER_ERROR = 0x26; + +// MMS temporary problem. +this.STK_RESULT_MMS_TEMPORARY = 0x27; + +// Results '3X' indicate that it is not worth the UICC re-trying with an +// identical command; as it will only get the same response. However, the +// decision to retry lies with the application. + +// Command beyond terminal's capabilities. +this.STK_RESULT_BEYOND_TERMINAL_CAPABILITY = 0x30; + +// Command type not understood by terminal. +this.STK_RESULT_CMD_TYPE_NOT_UNDERSTOOD = 0x31; + +// Command data not understood by terminal. +this.STK_RESULT_CMD_DATA_NOT_UNDERSTOOD = 0x32; + +// Command number not known by terminal. +this.STK_RESULT_CMD_NUM_NOT_KNOWN = 0x33; + +// SS Return Error. +this.STK_RESULT_SS_RETURN_ERROR = 0x34; + +// SMS RP-ERROR. +this.STK_RESULT_SMS_RP_ERROR = 0x35; + +// Error, required values are missing. +this.STK_RESULT_REQUIRED_VALUES_MISSING = 0x36; + +// USSD Return Error. +this.STK_RESULT_USSD_RETURN_ERROR = 0x37; + +// MultipleCard commands error. +this.STK_RESULT_MULTI_CARDS_CMD_ERROR = 0x38; + +// Interaction with call control by USIM or MO short message control by +// USIM; permanent problem. +this.STK_RESULT_USIM_CALL_CONTROL_PERMANENT = 0x39; + +// Bearer Independent Protocol error. +this.STK_RESULT_BIP_ERROR = 0x3a; + +// Access Technology unable to process command. +this.STK_RESULT_ACCESS_TECH_UNABLE_TO_PROCESS = 0x3b; + +// Frames error. +this.STK_RESULT_FRAMES_ERROR = 0x3c; + +// MMS Error. +this.STK_RESULT_MMS_ERROR = 0x3d; + +// STK presentation types, TS 11.14, clause 12.6, Command Qualifier: Select Item +this.STK_PRESENTATION_TYPE_NOT_SPECIFIED = 0x00; // Bit 1 is 0. +this.STK_PRESENTATION_TYPE_DATA_VALUES = 0x01; // Bit 1 is 1, bit 2 is 0. +this.STK_PRESENTATION_TYPE_NAVIGATION_OPTIONS = 0x03; // Bit 1 is 1, bit 2 is 1. + +// STK Coding Scheme. +this.STK_TEXT_CODING_GSM_7BIT_PACKED = 0x00; +this.STK_TEXT_CODING_GSM_8BIT = 0x04; +this.STK_TEXT_CODING_UCS2 = 0x08; + +// STK Event List. +this.STK_EVENT_TYPE_MT_CALL = 0x00; +this.STK_EVENT_TYPE_CALL_CONNECTED = 0x01; +this.STK_EVENT_TYPE_CALL_DISCONNECTED = 0x02; +this.STK_EVENT_TYPE_LOCATION_STATUS = 0x03; +this.STK_EVENT_TYPE_USER_ACTIVITY = 0x04; +this.STK_EVENT_TYPE_IDLE_SCREEN_AVAILABLE = 0x05; +this.STK_EVENT_TYPE_CARD_READER_STATUS = 0x06; +this.STK_EVENT_TYPE_LANGUAGE_SELECTION = 0x07; +this.STK_EVENT_TYPE_BROWSER_TERMINATION = 0x08; +this.STK_EVENT_TYPE_DATA_AVAILABLE = 0x09; +this.STK_EVENT_TYPE_CHANNEL_STATUS = 0x0a; +this.STK_EVENT_TYPE_SINGLE_ACCESS_TECHNOLOGY_CHANGED = 0x0b; +this.STK_EVENT_TYPE_DISPLAY_PARAMETER_CHANGED = 0x0c; +this.STK_EVENT_TYPE_LOCAL_CONNECTION = 0x0d; +this.STK_EVENT_TYPE_NETWORK_SEARCH_MODE_CHANGED = 0x0e; +this.STK_EVENT_TYPE_BROWSING_STATUS = 0x0f; + +// STK Service state of Location Status. +this.STK_SERVICE_STATE_NORMAL = 0x00; +this.STK_SERVICE_STATE_LIMITED = 0x01; +this.STK_SERVICE_STATE_UNAVAILABLE = 0x02; + +// Refresh mode. +this.STK_REFRESH_NAA_INIT_AND_FULL_FILE_CHANGE = 0x00; +this.STK_REFRESH_FILE_CHANGE = 0x01; +this.STK_REFRESH_NAA_INIT_AND_FILE_CHANGE = 0x02; +this.STK_REFRESH_NAA_INIT = 0x03; +this.STK_REFRESH_UICC_RESET = 0x04; + +// Tone type. +this.STK_TONE_TYPE_DIAL_TONE = 0x01; +this.STK_TONE_TYPE_CALLED_SUBSCRIBER_BUSY = 0x02; +this.STK_TONE_TYPE_CONGESTION = 0x03; +this.STK_TONE_TYPE_RADIO_PATH_ACK = 0x04; +this.STK_TONE_TYPE_RADIO_PATH_NOT_AVAILABLE = 0x05; +this.STK_TONE_TYPE_ERROR = 0x06; +this.STK_TONE_TYPE_CALL_WAITING_TONE = 0x07; +this.STK_TONE_TYPE_RINGING_TONE = 0x08; +this.STK_TONE_TYPE_GENERAL_BEEP = 0x10; +this.STK_TONE_TYPE_POSITIVE_ACK_TONE = 0x11; +this.STK_TONE_TYPE_NEGATIVE_ACK_TONE = 0x12; + +// Time unit. +this.STK_TIME_UNIT_MINUTE = 0x00; +this.STK_TIME_UNIT_SECOND = 0x01; +this.STK_TIME_UNIT_TENTH_SECOND = 0x02; + +// Local Information type. +this.STK_LOCAL_INFO_NNA = 0x00; +this.STK_LOCAL_INFO_IMEI = 0x01; +this.STK_LOCAL_INFO_NMR_FOR_NNA = 0x02; +this.STK_LOCAL_INFO_DATE_TIME_ZONE = 0x03; +this.STK_LOCAL_INFO_LANGUAGE = 0x04; +this.STK_LOCAL_INFO_ACCESS_TECH = 0x06; +this.STK_LOCAL_INFO_ESN = 0x07; +this.STK_LOCAL_INFO_IMEISV = 0x08; +this.STK_LOCAL_INFO_SEARCH_MODE = 0x09; +this.STK_LOCAL_INFO_CHARGE_STATE = 0x0A; +this.STK_LOCAL_INFO_MEID = 0x0B; +this.STK_LOCAL_INFO_BROADCAST_NETWORK_INFO = 0x0D; +this.STK_LOCAL_INFO_MULTIPLE_ACCESS_TECH = 0x0E; +this.STK_LOCAL_INFO_INFO_FOR_MULTIPLE_ACCESS_TECH = 0x0F; +this.STK_LOCAL_INFO_NMR_FOR_MULTIPLE_ACCESS_TECH = 0x10; + +// Timer Management. +this.STK_TIMER_START = 0x00; +this.STK_TIMER_DEACTIVATE = 0x01; +this.STK_TMIER_GET_CURRENT_VALUE = 0x02; + +// Browser Launch Mode. +this.STK_BROWSER_MODE_LAUNCH_IF_NOT_ALREADY_LAUNCHED = 0x00; +this.STK_BROWSER_MODE_USING_EXISTING_BROWSER = 0x02; +this.STK_BROWSER_MODE_USING_NEW_BROWSER = 0x03; + +// Browser Termination Cause. +this.STK_BROWSER_TERMINATION_CAUSE_USER = 0x00; +this.STK_BROWSER_TERMINATION_CAUSE_ERROR = 0x01; + +// Next Action Indicator. +this.STK_NEXT_ACTION_NULL = 0x00; +this.STK_NEXT_ACTION_END_PROACTIVE_SESSION = 0x81; + +/** + * Supported Terminal Facilities. + * + * value = 1, supported. + * 0, not supported. + */ +this.STK_TERMINAL_SUPPORT_PROFILE_DOWNLOAD = 1; +this.STK_TERMINAL_SUPPORT_SMS_PP_DOWNLOAD = 1; +this.STK_TERMINAL_SUPPORT_CELL_BROADCAST_DATA_DOWNLOAD = 0; +this.STK_TERMINAL_SUPPORT_MENU_SELECTION = 1; +this.STK_TERMINAL_SUPPORT_SIM_DATA_DOWNLOAD_ERROR = 0; +this.STK_TERMINAL_SUPPORT_TIMER_EXPIRATION = 1; +this.STK_TERMINAL_SUPPORT_USSD_IN_CALL_CONTROL = 0; +this.STK_TERMINAL_SUPPORT_CALL_CONTROL_IN_REDIAL = 0; + +this.STK_TERMINAL_SUPPORT_COMMAND_RESULT = 1; +this.STK_TERMINAL_SUPPORT_CALL_CONTROL = 1; +this.STK_TERMINAL_SUPPORT_CALL_ID_INCLUDED = 0; +this.STK_TERMINAL_SUPPORT_MO_SMS_CONTROL = 0; +this.STK_TERMINAL_SUPPORT_ALPHA_ID_INDICATION = 0; +this.STK_TERMINAL_SUPPORT_UCS2_ENTRY = 1; +this.STK_TERMINAL_SUPPORT_UCS2_DISPLAY = 1; +this.STK_TERMINAL_SUPPORT_EXTENSION_TEXT = 1; + +this.STK_TERMINAL_SUPPORT_PROACTIVE_DISPLAY_TEXT = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_GET_INKEY = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_GET_INPUT = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_MORE_TIME = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_PLAY_TONE = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_POLL_INTERVAL = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_POLL_OFF = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_REFRESH = 1; + +this.STK_TERMINAL_SUPPORT_PROACTIVE_SELECT_ITEM = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_SEND_SMS = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_SEND_SS = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_SEND_USSD = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_SET_UP_CALL = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_SET_UP_MENU = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_NMR = 0; + +this.STK_TERMINAL_SUPPORT_PROACTIVE_SET_UP_EVENT_LIST = 1; +this.STK_TERMINAL_SUPPORT_EVENT_MT_CALL = 1; +this.STK_TERMINAL_SUPPORT_EVENT_CALL_CONNECTED = 1; +this.STK_TERMINAL_SUPPORT_EVENT_CALL_DISCONNECTED = 1; +this.STK_TERMINAL_SUPPORT_EVENT_LOCATION_STATUS = 1; +this.STK_TERMINAL_SUPPORT_EVENT_USER_ACTIVITY = 1; +this.STK_TERMINAL_SUPPORT_EVENT_IDLE_SCREEN_AVAILABLE = 1; +this.STK_TERMINAL_SUPPORT_EVENT_CARD_READER_STATUS = 0; + +this.STK_TERMINAL_SUPPORT_EVENT_LANGUAGE_SELECTION = 1; +this.STK_TERMINAL_SUPPORT_EVENT_BROWSER_TERMINATION = 1; +this.STK_TERMINAL_SUPPORT_EVENT_DATA_AVAILABLE = 0; +this.STK_TERMINAL_SUPPORT_EVENT_CHANNEL_STATUS = 0; + +this.STK_TERMINAL_SUPPORT_PROACTIVE_TIMER_START_STOP = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_TIMER_GET_CURRENT = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_DATE = 1; +this.STK_TERMINAL_SUPPORT_GET_INKEY = 1; +this.STK_TERMINAL_SUPPORT_SET_UP_IDLE_MODE_TEXT = 1; +this.STK_TERMINAL_SUPPORT_RUN_AT_COMMAND = 0; +this.STK_TERMINAL_SUPPORT_SET_UP_CALL = 1; +this.STK_TERMINAL_SUPPORT_CALL_CONTROL_BY_NNA = 0; + +this.STK_TERMINAL_SUPPORT_DISPLAY_TEXT = 1; +this.STK_TERMINAL_SUPPORT_SEND_DTMF_COMMAND = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_NMR = 0; +this.STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_LANGUAGE = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_TIME_ADVANCE = 0; +this.STK_TERMINAL_SUPPORT_PROACTIVE_LANGUAGE_NOTIFICATION = 0; +this.STK_TERMINAL_SUPPORT_PROACTIVE_LAUNCH_BROWSER = 1; +this.STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_ACCESS_TECH = 0; + +this.STK_TERMINAL_SUPPORT_BIP_COMMAND_OPEN_CHANNEL = 1; +this.STK_TERMINAL_SUPPORT_BIP_COMMAND_CLOSE_CHANNEL = 1; +this.STK_TERMINAL_SUPPORT_BIP_COMMAND_RECEIVE_DATA = 1; +this.STK_TERMINAL_SUPPORT_BIP_COMMAND_SEND_DATA = 1; +this.STK_TERMINAL_SUPPORT_BIP_COMMAND_GET_CHANNEL_STATUS = 0; + +/** + * SAT profile + * + * @see ETSI TS 101.267, section 5.2. + */ +this.STK_TERMINAL_PROFILE_DOWNLOAD = + (STK_TERMINAL_SUPPORT_PROFILE_DOWNLOAD << 0) | + (STK_TERMINAL_SUPPORT_SMS_PP_DOWNLOAD << 1) | + (STK_TERMINAL_SUPPORT_CELL_BROADCAST_DATA_DOWNLOAD << 2) | + (STK_TERMINAL_SUPPORT_MENU_SELECTION << 3) | + (STK_TERMINAL_SUPPORT_SIM_DATA_DOWNLOAD_ERROR << 4) | + (STK_TERMINAL_SUPPORT_TIMER_EXPIRATION << 5) | + (STK_TERMINAL_SUPPORT_USSD_IN_CALL_CONTROL << 6) | + (STK_TERMINAL_SUPPORT_CALL_CONTROL_IN_REDIAL << 7); + +this.STK_TERMINAL_PROFILE_OTHER = + (STK_TERMINAL_SUPPORT_COMMAND_RESULT << 0) | + (STK_TERMINAL_SUPPORT_CALL_CONTROL << 1) | + (STK_TERMINAL_SUPPORT_CALL_ID_INCLUDED << 2) | + (STK_TERMINAL_SUPPORT_MO_SMS_CONTROL << 3) | + (STK_TERMINAL_SUPPORT_ALPHA_ID_INDICATION << 4) | + (STK_TERMINAL_SUPPORT_UCS2_ENTRY << 5) | + (STK_TERMINAL_SUPPORT_UCS2_DISPLAY << 6) | + (STK_TERMINAL_SUPPORT_EXTENSION_TEXT << 7); + +this.STK_TERMINAL_PROFILE_PROACTIVE_1 = + (STK_TERMINAL_SUPPORT_PROACTIVE_DISPLAY_TEXT << 0) | + (STK_TERMINAL_SUPPORT_PROACTIVE_GET_INKEY << 1) | + (STK_TERMINAL_SUPPORT_PROACTIVE_GET_INPUT << 2) | + (STK_TERMINAL_SUPPORT_PROACTIVE_MORE_TIME << 3) | + (STK_TERMINAL_SUPPORT_PROACTIVE_PLAY_TONE << 4) | + (STK_TERMINAL_SUPPORT_PROACTIVE_POLL_INTERVAL << 5) | + (STK_TERMINAL_SUPPORT_PROACTIVE_POLL_OFF << 6) | + (STK_TERMINAL_SUPPORT_PROACTIVE_REFRESH << 7); + +this.STK_TERMINAL_PROFILE_PROACTIVE_2 = + (STK_TERMINAL_SUPPORT_PROACTIVE_SELECT_ITEM << 0) | + (STK_TERMINAL_SUPPORT_PROACTIVE_SEND_SMS << 1) | + (STK_TERMINAL_SUPPORT_PROACTIVE_SEND_SS << 2) | + (STK_TERMINAL_SUPPORT_PROACTIVE_SEND_USSD << 3) | + (STK_TERMINAL_SUPPORT_PROACTIVE_SET_UP_CALL << 4) | + (STK_TERMINAL_SUPPORT_PROACTIVE_SET_UP_MENU << 5) | + (STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO << 6) | + (STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_NMR << 7); + +this.STK_TERMINAL_PROFILE_EVENT = + (STK_TERMINAL_SUPPORT_PROACTIVE_SET_UP_EVENT_LIST << 0) | + (STK_TERMINAL_SUPPORT_EVENT_MT_CALL << 1) | + (STK_TERMINAL_SUPPORT_EVENT_CALL_CONNECTED << 2) | + (STK_TERMINAL_SUPPORT_EVENT_CALL_DISCONNECTED << 3) | + (STK_TERMINAL_SUPPORT_EVENT_LOCATION_STATUS << 4) | + (STK_TERMINAL_SUPPORT_EVENT_USER_ACTIVITY << 5) | + (STK_TERMINAL_SUPPORT_EVENT_IDLE_SCREEN_AVAILABLE << 6) | + (STK_TERMINAL_SUPPORT_EVENT_CARD_READER_STATUS << 7); + +this.STK_TERMINAL_PROFILE_EVENT_EXT = + (STK_TERMINAL_SUPPORT_EVENT_LANGUAGE_SELECTION << 0) | + (STK_TERMINAL_SUPPORT_EVENT_BROWSER_TERMINATION << 1) | + (STK_TERMINAL_SUPPORT_EVENT_DATA_AVAILABLE << 2) | + (STK_TERMINAL_SUPPORT_EVENT_CHANNEL_STATUS << 3); + +this.STK_TERMINAL_PROFILE_PROACTIVE_3 = + (STK_TERMINAL_SUPPORT_PROACTIVE_TIMER_START_STOP << 0) | + (STK_TERMINAL_SUPPORT_PROACTIVE_TIMER_GET_CURRENT << 1) | + (STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_DATE << 2) | + (STK_TERMINAL_SUPPORT_GET_INKEY << 3) | + (STK_TERMINAL_SUPPORT_SET_UP_IDLE_MODE_TEXT << 4) | + (STK_TERMINAL_SUPPORT_RUN_AT_COMMAND << 5) | + (STK_TERMINAL_SUPPORT_SET_UP_CALL << 6) | + (STK_TERMINAL_SUPPORT_CALL_CONTROL_BY_NNA << 7); + +this.STK_TERMINAL_PROFILE_PROACTIVE_4 = + (STK_TERMINAL_SUPPORT_DISPLAY_TEXT << 0) | + (STK_TERMINAL_SUPPORT_SEND_DTMF_COMMAND << 1) | + (STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_NMR << 2) | + (STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_LANGUAGE << 3) | + (STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_TIME_ADVANCE << 4) | + (STK_TERMINAL_SUPPORT_PROACTIVE_LANGUAGE_NOTIFICATION << 5) | + (STK_TERMINAL_SUPPORT_PROACTIVE_LAUNCH_BROWSER << 6) | + (STK_TERMINAL_SUPPORT_PROACTIVE_LOCAL_INFO_ACCESS_TECH << 7); + +this.STK_TERMINAL_PROFILE_BIP_COMMAND = + (STK_TERMINAL_SUPPORT_BIP_COMMAND_OPEN_CHANNEL << 0) | + (STK_TERMINAL_SUPPORT_BIP_COMMAND_CLOSE_CHANNEL << 1) | + (STK_TERMINAL_SUPPORT_BIP_COMMAND_RECEIVE_DATA << 2) | + (STK_TERMINAL_SUPPORT_BIP_COMMAND_SEND_DATA << 3) | + (STK_TERMINAL_SUPPORT_BIP_COMMAND_GET_CHANNEL_STATUS << 4); + +this.STK_SUPPORTED_TERMINAL_PROFILE = [ + STK_TERMINAL_PROFILE_DOWNLOAD, + STK_TERMINAL_PROFILE_OTHER, + STK_TERMINAL_PROFILE_PROACTIVE_1, + STK_TERMINAL_PROFILE_PROACTIVE_2, + STK_TERMINAL_PROFILE_EVENT, + STK_TERMINAL_PROFILE_EVENT_EXT, // Event extension + 0x00, // Multiple card proactive commands + STK_TERMINAL_PROFILE_PROACTIVE_3, + STK_TERMINAL_PROFILE_PROACTIVE_4, + 0x00, // Softkey support + 0x00, // Softkey information + STK_TERMINAL_PROFILE_BIP_COMMAND, + 0x00, // BIP supported bearers + 0x00, // Screen height + 0x00, // Screen width + 0x00, // 16, Screen effects + 0x00, // 17, BIP supported transport interface + 0x00, // 18, RFU + 0x00, // 19, RFU + 0x00, // 20, RFU +]; + +/** + * ICC Services Table. + * + * @see 3GPP TS 51.011 10.3.7 (SIM) and 3GPP TS 31.102 4.2.8 (USIM). + */ +this.GECKO_ICC_SERVICES = { + // @see 3GPP TS 51.011 10.3.7 (SIM). + sim: { + ADN: 2, + FDN: 3, + PLMNSEL: 7, + MSISDN: 9, + EXT1: 10, + EXT2: 11, + CBMI: 14, + GID1: 15, + SPN: 17, + SDN: 18, + EXT3: 19, + DATA_DOWNLOAD_SMS_CB: 25, + DATA_DOWNLOAD_SMS_PP: 26, + CBMIR: 30, + BDN: 31, + IMG: 39, + PNN: 51, + OPL: 52, + MDN: 53, + MWIS: 54, + SPDI: 56 + }, + // @see 3GPP TS 31.102 4.2.8 (USIM). + usim: { + FDN: 2, + EXT2: 3, + SDN: 4, + EXT3: 5, + BDN: 6, + CBMI: 15, + CBMIR: 16, + GID1: 17, + SPN: 19, + MSISDN: 21, + IMG: 22, + DATA_DOWNLOAD_SMS_PP: 28, + DATA_DOWNLOAD_SMS_CB: 29, + PNN: 45, + OPL: 46, + MDN: 47, + MWIS: 48, + SPDI: 51 + }, + // @see 3GPP2 C.S0023-D 3.4.18 (RUIM). + ruim: { + FDN: 3, + ENHANCED_PHONEBOOK: 6, + EXT1: 10, + EXT2: 11, + SPN: 17, + SDN: 18, + EXT3: 19, + }, + // @see B.3.1.1 CPHS Information in CPHS Phase 2: + // Indicates which of the CPHS 'optional' data-fields are present in the SIM card: + // EF_CPHS_CSP, EF_CPHS_SST, EF_CPHS_MBN, EF_CPHS_ONSF, EF_CPHS_INFO_NUM + // Note: Mandatory EFs are: (B.3.1 Enhanced SIM Requirements) + // EF_CPHS_CFF, EF_CPHS_VMI, EF_CPHS_ONS, EF_CPHS_INFO + cphs: { + CSP: 1, + SST: 2, + MBN: 3, + ONSF: 4, + INFO_NUM: 5 + } +}; + +/** + * Cell Broadcast constants + */ + +this.CB_FORMAT_GSM = 0; +this.CB_FORMAT_ETWS = 1; +this.CB_FORMAT_CMAS = 2; +this.CB_FORMAT_UMTS = 3; + +// CBS Data Coding Scheme: Language groups +// see 3GPP TS 23.038 section 5 +this.CB_DCS_LANG_GROUP_1 = [ + "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", + "no", "el", "tr", "hu", "pl", null +]; +this.CB_DCS_LANG_GROUP_2 = [ + "cs", "he", "ar", "ru", "is", null, null, null, null, null, + null, null, null, null, null, null +]; + +// See 3GPP TS 23.041 v11.2.0 section 9.4.1.2.2 +this.CB_NON_MMI_SETTABLE_RANGES = [ + /*0x1000 - 0x107F*/4096, 4224, /*0x1080 - 0x10FF*/4224, 4352, + /*0x1112 - 0x1112*/4370, 4371, /*0x111F - 0x111F*/4383, 4384, + /*0xF000 - 0xFFFE*/61440, 65535, /*0xFFFF - 0xFFFF*/65535, 65536 +]; + +// User Data max length in septets +this.CB_MAX_CONTENT_7BIT = 93; +// User Data max length in octets +this.CB_MAX_CONTENT_8BIT = 82; +// User Data max length in chars +this.CB_MAX_CONTENT_UCS2 = 41; + +// See 3GPP TS 23.041 v11.6.0 senction 9.3.19 +this.CB_MSG_PAGE_INFO_SIZE = 82; + +this.CB_MESSAGE_SIZE_ETWS = 56; +this.CB_MESSAGE_SIZE_GSM = 88; +this.CB_MESSAGE_SIZE_UMTS_MIN = 90; +this.CB_MESSAGE_SIZE_UMTS_MAX = 1252; + + + +// GSM Cell Broadcast Geographical Scope +// See 3GPP TS 23.041 clause 9.4.1.2.1 +this.CB_GSM_GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE = 0; +this.CB_GSM_GEOGRAPHICAL_SCOPE_PLMN_WIDE = 1; +this.CB_GSM_GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE = 2; +this.CB_GSM_GEOGRAPHICAL_SCOPE_CELL_WIDE = 3; + +// GSM Cell Broadcast Geographical Scope +// See 3GPP TS 23.041 clause 9.4.1.2.1 +this.CB_GSM_GEOGRAPHICAL_SCOPE_NAMES = [ + "cell-immediate", + "plmn", + "location-area", + "cell" +]; + +// GSM Cell Broadcast Message Identifiers +// see 3GPP TS 23.041 clause 9.4.1.2.2 +this.CB_GSM_MESSAGEID_ETWS_BEGIN = 0x1100; +this.CB_GSM_MESSAGEID_ETWS_END = 0x1107; + +// ETWS Warning-Type +// see 3GPP TS 23.041 clause 9.3.24 +this.CB_ETWS_WARNING_TYPE_NAMES = [ + "earthquake", + "tsunami", + "earthquake-tsunami", + "test", + "other" +]; + +// UMTS Message Type +// see 3GPP TS 25.324 section 11.1 +this.CB_UMTS_MESSAGE_TYPE_CBS = 1; +this.CB_UMTS_MESSAGE_TYPE_SCHEDULE = 2; +this.CB_UMTS_MESSAGE_TYPE_CBS41 = 3; + +/** + * Number plan identification defined in + * |Table 10.5.118: Called party BCD number| of 3GPP TS 24.008. + */ +this.CALLED_PARTY_BCD_NPI_UNKNOWN = 0; +this.CALLED_PARTY_BCD_NPI_ISDN = 1; +this.CALLED_PARTY_BCD_NPI_DATA = 3; +this.CALLED_PARTY_BCD_NPI_TELEX = 4; +this.CALLED_PARTY_BCD_NPI_NATIONAL = 8; +this.CALLED_PARTY_BCD_NPI_PRIVATE = 9; + +/** + * Array of number plan identification values which can be used to map an + * enumeration to the corresponding value. The indices should be consistent + * with nsISmsService::NUMBER_PLAN_IDENTIFICATION_* constants. + */ +this.CALLED_PARTY_BCD_NPI = [ + CALLED_PARTY_BCD_NPI_UNKNOWN, + CALLED_PARTY_BCD_NPI_ISDN, + CALLED_PARTY_BCD_NPI_DATA, + CALLED_PARTY_BCD_NPI_TELEX, + CALLED_PARTY_BCD_NPI_NATIONAL, + CALLED_PARTY_BCD_NPI_PRIVATE +]; + +/** + * GSM PDU constants + */ + +// PDU TYPE-OF-ADDRESS +this.PDU_TOA_UNKNOWN = 0x80; // Unknown. This is used when the user or + // network has no a priori information + // about the numbering plan. +this.PDU_TOA_ISDN = 0x81; // ISDN/Telephone numbering +this.PDU_TOA_DATA_NUM = 0x83; // Data numbering plan +this.PDU_TOA_TELEX_NUM = 0x84; // Telex numbering plan +this.PDU_TOA_NATIONAL_NUM = 0x88; // National numbering plan +this.PDU_TOA_PRIVATE_NUM = 0x89; // Private numbering plan +this.PDU_TOA_ERMES_NUM = 0x8A; // Ermes numbering plan +this.PDU_TOA_INTERNATIONAL = 0x90; // International number +this.PDU_TOA_NATIONAL = 0xA0; // National number. Prefix or escape digits + // shall not be included +this.PDU_TOA_NETWORK_SPEC = 0xB0; // Network specific number This is used to + // indicate administration/service number + // specific to the serving network +this.PDU_TOA_SUBSCRIBER = 0xC0; // Subscriber number. This is used when a + // specific short number representation is + // stored in one or more SCs as part of a + // higher layer application +this.PDU_TOA_ALPHANUMERIC = 0xD0; // Alphanumeric, (coded according to GSM TS + // 03.38 7-bit default alphabet) +this.PDU_TOA_ABBREVIATED = 0xE0; // Abbreviated number + +/** + * First octet of the SMS-DELIVER PDU + * + * RP: 0 Reply Path parameter is not set in this PDU + * 1 Reply Path parameter is set in this PDU + * + * UDHI: 0 The UD field contains only the short message + * 1 The beginning of the UD field contains a header in addition of + * the short message + * + * SRI: (is only set by the SMSC) + * 0 A status report will not be returned to the SME + * 1 A status report will be returned to the SME + * + * MMS: (is only set by the SMSC) + * 0 More messages are waiting for the MS in the SMSC + * 1 No more messages are waiting for the MS in the SMSC + * + * MTI: bit1 bit0 Message type + * 0 0 SMS-DELIVER (SMSC ==> MS) + * 0 0 SMS-DELIVER REPORT (MS ==> SMSC, is generated + * automatically by the M20, after receiving a + * SMS-DELIVER) + * 0 1 SMS-SUBMIT (MS ==> SMSC) + * 0 1 SMS-SUBMIT REPORT (SMSC ==> MS) + * 1 0 SMS-STATUS REPORT (SMSC ==> MS) + * 1 0 SMS-COMMAND (MS ==> SMSC) + * 1 1 Reserved + */ +this.PDU_RP = 0x80; // Reply path. Parameter indicating that + // reply path exists. +this.PDU_UDHI = 0x40; // User data header indicator. This bit is + // set to 1 if the User Data field starts + // with a header +this.PDU_SRI_SRR = 0x20; // Status report indication (SMS-DELIVER) + // or request (SMS-SUBMIT) +this.PDU_VPF_ABSOLUTE = 0x18;// Validity period aboslute format + // (SMS-SUBMIT only) +this.PDU_VPF_RELATIVE = 0x10;// Validity period relative format + // (SMS-SUBMIT only) +this.PDU_VPF_ENHANCED = 0x8; // Validity period enhance format + // (SMS-SUBMIT only) +this.PDU_MMS_RD = 0x04;// More messages to send. (SMS-DELIVER only) or + // Reject duplicates (SMS-SUBMIT only) + +// MTI - Message Type Indicator +this.PDU_MTI_SMS_RESERVED = 0x03; +this.PDU_MTI_SMS_STATUS_REPORT = 0x02; +this.PDU_MTI_SMS_COMMAND = 0x02; +this.PDU_MTI_SMS_SUBMIT = 0x01; +this.PDU_MTI_SMS_DELIVER = 0x00; + +// PI - Parameter Indicator +this.PDU_PI_EXTENSION = 0x80; +this.PDU_PI_USER_DATA_LENGTH = 0x04; +this.PDU_PI_DATA_CODING_SCHEME = 0x02; +this.PDU_PI_PROTOCOL_IDENTIFIER = 0x01; +this.PDU_PI_RESERVED = 0x78; + +// FCS - Failure Cause +// 0...127 see 3GPP TS 24.011 clause E.2 +// 128...255 see 3GPP TS 23.040 clause 9.2.3.22 +// others see 3GPP TS 27.005 clause 3.2.5 +this.PDU_FCS_OK = 0x00; +this.PDU_FCS_PROTOCOL_ERROR = 0x6F; +this.PDU_FCS_MEMORY_CAPACITY_EXCEEDED = 0XD3; +this.PDU_FCS_USAT_BUSY = 0XD4; +this.PDU_FCS_USIM_DATA_DOWNLOAD_ERROR = 0xD5; +this.PDU_FCS_RESERVED = 0xE0; +this.PDU_FCS_UNSPECIFIED = 0xFF; +// Special internal value that means we should not acknowledge an +// incoming text right away, but need to wait for other components +// (e.g. storage) to complete. This can be any value, so long it +// doesn't conflict with the PDU_FCS_* constants above. +this.MOZ_FCS_WAIT_FOR_EXPLICIT_ACK = 0x0F; + +// ST - Status +// Bit 7..0 = 000xxxxx, short message transaction completed +this.PDU_ST_0_RECEIVED = 0x00; +this.PDU_ST_0_FORWARDED_NO_CONFIRM = 0x01; +this.PDU_ST_0_REPLACED_BY_SC = 0x02; +this.PDU_ST_0_RESERVED_BEGIN = 0x03; +this.PDU_ST_0_SC_SPECIFIC_BEGIN = 0x10; +this.PDU_ST_0_SC_SPECIFIC_END = 0x1F; +// Bit 7..0 = 001xxxxx, temporary error, SC still trying to transfer SM +this.PDU_ST_1_CONGESTION = 0x20; +this.PDU_ST_1_SME_BUSY = 0x21; +this.PDU_ST_1_SME_NO_RESPONSE = 0x22; +this.PDU_ST_1_SERVICE_REJECTED = 0x23; +this.PDU_ST_1_QOS_UNAVAILABLE = 0x24; +this.PDU_ST_1_SME_ERROR = 0x25; +this.PDU_ST_1_RESERVED_BEGIN = 0x26; +this.PDU_ST_1_SC_SPECIFIC_BEGIN = 0x30; +this.PDU_ST_1_SC_SPECIFIC_END = 0x3F; +// Bit 7..0 = 010xxxxx, permanent error, SC is not making any more transfer +// attempts +this.PDU_ST_2_RPC_ERROR = 0x40; +this.PDU_ST_2_DEST_INCOMPATIBLE = 0x41; +this.PDU_ST_2_CONNECTION_REJECTED = 0x42; +this.PDU_ST_2_NOT_OBTAINABLE = 0x43; +this.PDU_ST_2_QOS_UNAVAILABLE = 0x44; +this.PDU_ST_2_INTERWORKING_UNAVALIABLE = 0x45; +this.PDU_ST_2_VALIDITY_EXPIRED = 0x46; +this.PDU_ST_2_DELETED_BY_SME = 0x47; +this.PDU_ST_2_DELETED_BY_SC = 0x48; +this.PDU_ST_2_SM_MISSING = 0x49; +this.PDU_ST_2_RESERVED_BEGIN = 0x4A; +this.PDU_ST_2_SC_SPECIFIC_BEGIN = 0x50; +this.PDU_ST_2_SC_SPECIFIC_END = 0x5F; +// Bit 7..0 = 011xxxxx, temporary error, SC is not making any more transfer +// attempts +this.PDU_ST_3_CONGESTION = 0x60; +this.PDU_ST_3_SME_BUSY = 0x61; +this.PDU_ST_3_SME_NO_RESPONSE = 0x62; +this.PDU_ST_3_SERVICE_REJECTED = 0x63; +this.PDU_ST_3_QOS_UNAVAILABLE = 0x64; +this.PDU_ST_3_SME_ERROR = 0x65; +this.PDU_ST_3_RESERVED_BEGIN = 0x66; +this.PDU_ST_3_SC_SPECIFIC_BEGIN = 0x70; +this.PDU_ST_3_SC_SPECIFIC_END = 0x7F; + +this.GECKO_SMS_DELIVERY_STATUS_NOT_APPLICABLE = "not-applicable"; +this.GECKO_SMS_DELIVERY_STATUS_SUCCESS = "success"; +this.GECKO_SMS_DELIVERY_STATUS_PENDING = "pending"; +this.GECKO_SMS_DELIVERY_STATUS_ERROR = "error"; + +// User Data max length in septets +this.PDU_MAX_USER_DATA_7BIT = 160; +// User Data max length in octets +this.PDU_MAX_USER_DATA_8BIT = 140; +// User Data max length in chars +this.PDU_MAX_USER_DATA_UCS2 = 70; + +// PID - Protocol Indicator +this.PDU_PID_DEFAULT = 0x00; +this.PDU_PID_TELEMATIC_INTERWORKING = 0x20; +this.PDU_PID_SHORT_MESSAGE_TYPE_0 = 0x40; +this.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_1 = 0x41; +this.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_2 = 0x42; +this.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_3 = 0x43; +this.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_4 = 0x44; +this.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_5 = 0x45; +this.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_6 = 0x46; +this.PDU_PID_REPLACE_SHORT_MESSAGE_TYPE_7 = 0x47; +this.PDU_PID_ENHANDED_MESSAGE_SERVICE = 0x5E; +this.PDU_PID_RETURN_CALL_MESSAGE = 0x5F; +this.PDU_PID_ANSI_136_R_DATA = 0x7C; +this.PDU_PID_ME_DATA_DOWNLOAD = 0x7D; +this.PDU_PID_ME_DEPERSONALIZATION = 0x7E; +this.PDU_PID_USIM_DATA_DOWNLOAD = 0x7F; + +// DCS - Data Coding Scheme +this.PDU_DCS_MSG_CODING_7BITS_ALPHABET = 0x00; +this.PDU_DCS_MSG_CODING_8BITS_ALPHABET = 0x04; +this.PDU_DCS_MSG_CODING_16BITS_ALPHABET = 0x08; +this.PDU_DCS_MSG_CLASS_0 = 0x00; +this.PDU_DCS_MSG_CLASS_1 = 0x01; +this.PDU_DCS_MSG_CLASS_2 = 0x02; +this.PDU_DCS_MSG_CLASS_3 = 0x03; +this.PDU_DCS_MSG_CLASS_USER_1 = 0x04; +this.PDU_DCS_MSG_CLASS_USER_2 = 0x05; +this.PDU_DCS_MSG_CLASS_NORMAL = 0x06; +this.PDU_DCS_CODING_GROUP_BITS = 0xF0; +this.PDU_DCS_MSG_CLASS_BITS = 0x03; +this.PDU_DCS_MWI_ACTIVE_BITS = 0x08; +this.PDU_DCS_MWI_ACTIVE_VALUE = 0x08; +this.PDU_DCS_MWI_TYPE_BITS = 0x03; +this.PDU_DCS_MWI_TYPE_VOICEMAIL = 0x00; +this.PDU_DCS_MWI_TYPE_FAX = 0x01; +this.PDU_DCS_MWI_TYPE_EMAIL = 0x02; +this.PDU_DCS_MWI_TYPE_OTHER = 0x03; + +// Set as Array instead of Object for reversed-mapping with Array.indexOf(). +this.GECKO_SMS_MESSAGE_CLASSES = []; +GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0] = "class-0"; +GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_1] = "class-1"; +GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_2] = "class-2"; +GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_3] = "class-3"; +GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_1] = "user-1"; +GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_USER_2] = "user-2"; +GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL] = "normal"; + +// Because service center timestamp omit the century. Yay. +this.PDU_TIMESTAMP_YEAR_OFFSET = 2000; + +// See 9.2.3.24 TP‑User Data (TP‑UD) +this.PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT = 0x00; +this.PDU_IEI_SPECIAL_SMS_MESSAGE_INDICATION = 0x01; +this.PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_8BIT = 0x04; +this.PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_16BIT = 0x05; +this.PDU_IEI_SMSC_CONTROL_PARAMS = 0x06; +this.PDU_IEI_UDH_SOURCE_INDICATOR = 0x07; +this.PDU_IEI_CONCATENATED_SHORT_MESSAGES_16BIT = 0x08; +this.PDU_IEI_WIRELESS_CONTROL_MESSAGE_PROTOCOL = 0x09; +this.PDU_IEI_TEXT_FORMATING = 0x0A; +this.PDU_IEI_PREDEFINED_SOUND = 0x0B; +this.PDU_IEI_USER_DATA_SOUND = 0x0C; +this.PDU_IEI_PREDEFINED_ANIMATION = 0x0D; +this.PDU_IEI_LARGE_ANIMATION = 0x0E; +this.PDU_IEI_SMALL_ANIMATION = 0x0F; +this.PDU_IEI_LARGE_PICTURE = 0x10; +this.PDU_IEI_SMALL_PICTURE = 0x11; +this.PDU_IEI_VARIABLE_PICTURE = 0x12; +this.PDU_IEI_USER_PROMPT_INDICATOR = 0x13; +this.PDU_IEI_EXTENDED_OBJECT = 0x14; +this.PDU_IEI_REUSED_EXTENDED_OBJECT = 0x15; +this.PDU_IEI_COMPRESS_CONTROL = 0x16; +this.PDU_IEI_OBJECT_DISTRIBUTION_INDICATOR = 0x17; +this.PDU_IEI_STANDARD_WVG_OBJECT = 0x18; +this.PDU_IEI_CHARACTER_SIZE_WVG_OBJECT = 0x19; +this.PDU_IEI_EXTENDED_OBJECT_DATA_REQUEST_COMMAND = 0x1A; +this.PDU_IEI_RFC822_EMAIL_HEADER = 0x20; +this.PDU_IEI_HYPERLINK_FORMAT_ELEMENT = 0x21; +this.PDU_IEI_REPLY_ADDRESS_ELEMENT = 0x22; +this.PDU_IEI_ENHANCED_VOICE_MAIL_INFORMATION = 0x23; +this.PDU_IEI_NATIONAL_LANGUAGE_SINGLE_SHIFT = 0x24; +this.PDU_IEI_NATIONAL_LANGUAGE_LOCKING_SHIFT = 0x25; + +// Application Port Addressing, see 3GPP TS 23.040 9.2.3.24.3 +this.PDU_APA_RESERVED_8BIT_PORTS = 240; +this.PDU_APA_VALID_16BIT_PORTS = 49152; + +// 7bit alphabet escape character. The encoded value of this code point is left +// undefined in official spec. Its code value is internally assigned to \uffff, +// <noncharacter-FFFF> in Unicode basic multilingual plane. +this.PDU_NL_EXTENDED_ESCAPE = 0x1B; + +// <SP>, <LF>, <CR> are only defined in locking shift tables. +this.PDU_NL_SPACE = 0x20; +this.PDU_NL_LINE_FEED = 0x0A; +this.PDU_NL_CARRIAGE_RETURN = 0x0D; + +// 7bit alphabet page break character, only defined in single shift tables. +// The encoded value of this code point is left undefined in official spec, but +// the code point itself maybe be used for example in compressed CBS messages. +// Its code value is internally assigned to \u000c, ASCII form feed, or new page. +this.PDU_NL_PAGE_BREAK = 0x0A; +// 7bit alphabet reserved control character, only defined in single shift +// tables. The encoded value of this code point is left undefined in official +// spec. Its code value is internally assigned to \ufffe, <noncharacter-FFFE> +// in Unicode basic multilingual plane. +this.PDU_NL_RESERVED_CONTROL = 0x0D; + +this.PDU_NL_IDENTIFIER_DEFAULT = 0; +this.PDU_NL_IDENTIFIER_TURKISH = 1; +this.PDU_NL_IDENTIFIER_SPANISH = 2; +this.PDU_NL_IDENTIFIER_PORTUGUESE = 3; +this.PDU_NL_IDENTIFIER_BENGALI = 4; +this.PDU_NL_IDENTIFIER_GUJARATI = 5; +this.PDU_NL_IDENTIFIER_HINDI = 6; +this.PDU_NL_IDENTIFIER_KANNADA = 7; +this.PDU_NL_IDENTIFIER_MALAYALAM = 8; +this.PDU_NL_IDENTIFIER_ORIYA = 9; +this.PDU_NL_IDENTIFIER_PUNJABI = 10; +this.PDU_NL_IDENTIFIER_TAMIL = 11; +this.PDU_NL_IDENTIFIER_TELUGU = 12; +this.PDU_NL_IDENTIFIER_URDU = 13; + +// The mapping of mcc and their extra GSM national language locking / single +// shift table tuples to enable. The default GSM alphabet and extension table +// are always enabled and need not to be list here. +// +// The content should be updated when a relevant national regulatory body +// requests. See 'NOTE 2' of 6.2.1.2.5 in 3GPP TS 23.038: +// " +// Encoding of a message using the national locking shift mechanism is not +// intended to be implemented until a formal request is issued by the +// relevant national regulatory body. This is because a receiving entity +// not supporting the relevant locking-shift decoding will present different +// characters from the ones intended by the sending entity. +// " +this.PDU_MCC_NL_TABLE_TUPLES_MAPPING = { + // Configuration for Turkey. + // + // The Turkish single shift table contains 7 extra characters + // (Ğ, İ, Ş, ç, ğ, ı, ş) than the GSM default alphabet extension table. Since + // all the 7 characters are also included in Turkish locking shift table, it's + // not necessary to enable Turkish single shift table. Using GSM default + // alphabet extension table instead saves 3 octets when these extension table + // characters present in a message. + 286: [[PDU_NL_IDENTIFIER_TURKISH, PDU_NL_IDENTIFIER_DEFAULT]] +}; + +/* + * 3GPP TS 23.038 - 6.2.1 GSM 7 bit Default Alphabet + */ +this.PDU_NL_GSM_DEFAULT_ALPHABET = + // 01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F..... + "@\u00a3$\u00a5\u00e8\u00e9\u00f9\u00ec\u00f2\u00c7\n\u00d8\u00f8\r\u00c5\u00e5" + // 0.....12.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0394_\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\uffff\u00c6\u00e6\u00df\u00c9" + // 012.34.....56789ABCDEF + + " !\"#\u00a4%&'()*+,-./" + // 0123456789ABCDEF + + "0123456789:;<=>?" + // 0.....123456789ABCDEF + + "\u00a1ABCDEFGHIJKLMNO" + // 0123456789AB.....C.....D.....E.....F..... + + "PQRSTUVWXYZ\u00c4\u00d6\u00d1\u00dc\u00a7" + // 0.....123456789ABCDEF + + "\u00bfabcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0"; + +// National Language Locking Shift Tables, see 3GPP TS 23.038 +this.PDU_NL_LOCKING_SHIFT_TABLES = [ + /** + * National Language Identifier: 0x00 + * 6.2.1 GSM 7 bit Default Alphabet + */ + PDU_NL_GSM_DEFAULT_ALPHABET, + + /** + * National Language Identifier: 0x01 + * A.3.1 Turkish National Language Locking Shift Table + */ + // 01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F..... + "@\u00a3$\u00a5\u20ac\u00e9\u00f9\u0131\u00f2\u00c7\n\u011e\u011f\r\u00c5\u00e5" + // 0.....12.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0394_\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\uffff\u015e\u015f\u00df\u00c9" + // 012.34.....56789ABCDEF + + " !\"#\u00a4%&'()*+,-./" + // 0123456789ABCDEF + + "0123456789:;<=>?" + // 0.....123456789ABCDEF + + "\u0130ABCDEFGHIJKLMNO" + // 0123456789AB.....C.....D.....E.....F..... + + "PQRSTUVWXYZ\u00c4\u00d6\u00d1\u00dc\u00a7" + // 0.....123456789ABCDEF + + "\u00e7abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0", + + /** + * National Language Identifier: 0x02 + * A.3.2 Void + * Fallback to GSM Default Alphabet + */ + PDU_NL_GSM_DEFAULT_ALPHABET, + + /** + * National Language Identifier: 0x03 + * A.3.3 Portuguese National Language Locking Shift Table + */ + // 01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F..... + "@\u00a3$\u00a5\u00ea\u00e9\u00fa\u00ed\u00f3\u00e7\n\u00d4\u00f4\r\u00c1\u00e1" + // 0.....12.....3.....4.....5.....67.8.....9.....AB.....C.....D.....E.....F..... + + "\u0394_\u00aa\u00c7\u00c0\u221e^\\\u20ac\u00d3|\uffff\u00c2\u00e2\u00ca\u00c9" + // 012.34.....56789ABCDEF + + " !\"#\u00ba%&'()*+,-./" + // 0123456789ABCDEF + + "0123456789:;<=>?" + // 0.....123456789ABCDEF + + "\u00cdABCDEFGHIJKLMNO" + // 0123456789AB.....C.....D.....E.....F..... + + "PQRSTUVWXYZ\u00c3\u00d5\u00da\u00dc\u00a7" + // 0123456789ABCDEF + + "~abcdefghijklmno" + // 0123456789AB.....C.....DE.....F..... + + "pqrstuvwxyz\u00e3\u00f5`\u00fc\u00e0", + + /** + * National Language Identifier: 0x04 + * A.3.4 Bengali National Language Locking Shift Table + */ + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.EF..... + "\u0981\u0982\u0983\u0985\u0986\u0987\u0988\u0989\u098a\u098b\n\u098c \r \u098f" + // 0.....123.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0990 \u0993\u0994\u0995\u0996\u0997\u0998\u0999\u099a\uffff\u099b\u099c\u099d\u099e" + // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF..... + + " !\u099f\u09a0\u09a1\u09a2\u09a3\u09a4)(\u09a5\u09a6,\u09a7.\u09a8" + // 0123456789ABCD.....E.....F + + "0123456789:; \u09aa\u09ab?" + // 0.....1.....2.....3.....4.....56.....789A.....B.....C.....D.....E.....F..... + + "\u09ac\u09ad\u09ae\u09af\u09b0 \u09b2 \u09b6\u09b7\u09b8\u09b9\u09bc\u09bd" + // 0.....1.....2.....3.....4.....5.....6.....789.....A.....BCD.....E.....F..... + + "\u09be\u09bf\u09c0\u09c1\u09c2\u09c3\u09c4 \u09c7\u09c8 \u09cb\u09cc\u09cd" + // 0.....123456789ABCDEF + + "\u09ceabcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u09d7\u09dc\u09dd\u09f0\u09f1", + + /** + * National Language Identifier: 0x05 + * A.3.5 Gujarati National Language Locking Shift Table + */ + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.EF..... + "\u0a81\u0a82\u0a83\u0a85\u0a86\u0a87\u0a88\u0a89\u0a8a\u0a8b\n\u0a8c\u0a8d\r \u0a8f" + // 0.....1.....23.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0a90\u0a91 \u0a93\u0a94\u0a95\u0a96\u0a97\u0a98\u0a99\u0a9a\uffff\u0a9b\u0a9c\u0a9d\u0a9e" + // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF..... + + " !\u0a9f\u0aa0\u0aa1\u0aa2\u0aa3\u0aa4)(\u0aa5\u0aa6,\u0aa7.\u0aa8" + // 0123456789ABCD.....E.....F + + "0123456789:; \u0aaa\u0aab?" + // 0.....1.....2.....3.....4.....56.....7.....89.....A.....B.....C.....D.....E.....F..... + + "\u0aac\u0aad\u0aae\u0aaf\u0ab0 \u0ab2\u0ab3 \u0ab5\u0ab6\u0ab7\u0ab8\u0ab9\u0abc\u0abd" + // 0.....1.....2.....3.....4.....5.....6.....7.....89.....A.....B.....CD.....E.....F..... + + "\u0abe\u0abf\u0ac0\u0ac1\u0ac2\u0ac3\u0ac4\u0ac5 \u0ac7\u0ac8\u0ac9 \u0acb\u0acc\u0acd" + // 0.....123456789ABCDEF + + "\u0ad0abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u0ae0\u0ae1\u0ae2\u0ae3\u0af1", + + /** + * National Language Identifier: 0x06 + * A.3.6 Hindi National Language Locking Shift Table + */ + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F..... + "\u0901\u0902\u0903\u0905\u0906\u0907\u0908\u0909\u090a\u090b\n\u090c\u090d\r\u090e\u090f" + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0910\u0911\u0912\u0913\u0914\u0915\u0916\u0917\u0918\u0919\u091a\uffff\u091b\u091c\u091d\u091e" + // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF..... + + " !\u091f\u0920\u0921\u0922\u0923\u0924)(\u0925\u0926,\u0927.\u0928" + // 0123456789ABC.....D.....E.....F + + "0123456789:;\u0929\u092a\u092b?" + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u092c\u092d\u092e\u092f\u0930\u0931\u0932\u0933\u0934\u0935\u0936\u0937\u0938\u0939\u093c\u093d" + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u093e\u093f\u0940\u0941\u0942\u0943\u0944\u0945\u0946\u0947\u0948\u0949\u094a\u094b\u094c\u094d" + // 0.....123456789ABCDEF + + "\u0950abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u0972\u097b\u097c\u097e\u097f", + + /** + * National Language Identifier: 0x07 + * A.3.7 Kannada National Language Locking Shift Table + */ + // 01.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.E.....F..... + " \u0c82\u0c83\u0c85\u0c86\u0c87\u0c88\u0c89\u0c8a\u0c8b\n\u0c8c \r\u0c8e\u0c8f" + // 0.....12.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0c90 \u0c92\u0c93\u0c94\u0c95\u0c96\u0c97\u0c98\u0c99\u0c9a\uffff\u0c9b\u0c9c\u0c9d\u0c9e" + // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF..... + + " !\u0c9f\u0ca0\u0ca1\u0ca2\u0ca3\u0ca4)(\u0ca5\u0ca6,\u0ca7.\u0ca8" + // 0123456789ABCD.....E.....F + + "0123456789:; \u0caa\u0cab?" + // 0.....1.....2.....3.....4.....5.....6.....7.....89.....A.....B.....C.....D.....E.....F..... + + "\u0cac\u0cad\u0cae\u0caf\u0cb0\u0cb1\u0cb2\u0cb3 \u0cb5\u0cb6\u0cb7\u0cb8\u0cb9\u0cbc\u0cbd" + // 0.....1.....2.....3.....4.....5.....6.....78.....9.....A.....BC.....D.....E.....F..... + + "\u0cbe\u0cbf\u0cc0\u0cc1\u0cc2\u0cc3\u0cc4 \u0cc6\u0cc7\u0cc8 \u0cca\u0ccb\u0ccc\u0ccd" + // 0.....123456789ABCDEF + + "\u0cd5abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u0cd6\u0ce0\u0ce1\u0ce2\u0ce3", + + /** + * National Language Identifier: 0x08 + * A.3.8 Malayalam National Language Locking Shift Table + */ + // 01.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.E.....F..... + " \u0d02\u0d03\u0d05\u0d06\u0d07\u0d08\u0d09\u0d0a\u0d0b\n\u0d0c \r\u0d0e\u0d0f" + // 0.....12.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0d10 \u0d12\u0d13\u0d14\u0d15\u0d16\u0d17\u0d18\u0d19\u0d1a\uffff\u0d1b\u0d1c\u0d1d\u0d1e" + // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF..... + + " !\u0d1f\u0d20\u0d21\u0d22\u0d23\u0d24)(\u0d25\u0d26,\u0d27.\u0d28" + // 0123456789ABCD.....E.....F + + "0123456789:; \u0d2a\u0d2b?" + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....EF..... + + "\u0d2c\u0d2d\u0d2e\u0d2f\u0d30\u0d31\u0d32\u0d33\u0d34\u0d35\u0d36\u0d37\u0d38\u0d39 \u0d3d" + // 0.....1.....2.....3.....4.....5.....6.....78.....9.....A.....BC.....D.....E.....F..... + + "\u0d3e\u0d3f\u0d40\u0d41\u0d42\u0d43\u0d44 \u0d46\u0d47\u0d48 \u0d4a\u0d4b\u0d4c\u0d4d" + // 0.....123456789ABCDEF + + "\u0d57abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u0d60\u0d61\u0d62\u0d63\u0d79", + + /** + * National Language Identifier: 0x09 + * A.3.9 Oriya National Language Locking Shift Table + */ + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.EF..... + "\u0b01\u0b02\u0b03\u0b05\u0b06\u0b07\u0b08\u0b09\u0b0a\u0b0b\n\u0b0c \r \u0b0f" + // 0.....123.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0b10 \u0b13\u0b14\u0b15\u0b16\u0b17\u0b18\u0b19\u0b1a\uffff\u0b1b\u0b1c\u0b1d\u0b1e" + // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF..... + + " !\u0b1f\u0b20\u0b21\u0b22\u0b23\u0b24)(\u0b25\u0b26,\u0b27.\u0b28" + // 0123456789ABCD.....E.....F + + "0123456789:; \u0b2a\u0b2b?" + // 0.....1.....2.....3.....4.....56.....7.....89.....A.....B.....C.....D.....E.....F..... + + "\u0b2c\u0b2d\u0b2e\u0b2f\u0b30 \u0b32\u0b33 \u0b35\u0b36\u0b37\u0b38\u0b39\u0b3c\u0b3d" + // 0.....1.....2.....3.....4.....5.....6.....789.....A.....BCD.....E.....F..... + + "\u0b3e\u0b3f\u0b40\u0b41\u0b42\u0b43\u0b44 \u0b47\u0b48 \u0b4b\u0b4c\u0b4d" + // 0.....123456789ABCDEF + + "\u0b56abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u0b57\u0b60\u0b61\u0b62\u0b63", + + /** + * National Language Identifier: 0x0A + * A.3.10 Punjabi National Language Locking Shift Table + */ + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9A.BCD.EF..... + "\u0a01\u0a02\u0a03\u0a05\u0a06\u0a07\u0a08\u0a09\u0a0a \n \r \u0a0f" + // 0.....123.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0a10 \u0a13\u0a14\u0a15\u0a16\u0a17\u0a18\u0a19\u0a1a\uffff\u0a1b\u0a1c\u0a1d\u0a1e" + // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF..... + + " !\u0a1f\u0a20\u0a21\u0a22\u0a23\u0a24)(\u0a25\u0a26,\u0a27.\u0a28" + // 0123456789ABCD.....E.....F + + "0123456789:; \u0a2a\u0a2b?" + // 0.....1.....2.....3.....4.....56.....7.....89.....A.....BC.....D.....E.....F + + "\u0a2c\u0a2d\u0a2e\u0a2f\u0a30 \u0a32\u0a33 \u0a35\u0a36 \u0a38\u0a39\u0a3c " + // 0.....1.....2.....3.....4.....56789.....A.....BCD.....E.....F..... + + "\u0a3e\u0a3f\u0a40\u0a41\u0a42 \u0a47\u0a48 \u0a4b\u0a4c\u0a4d" + // 0.....123456789ABCDEF + + "\u0a51abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u0a70\u0a71\u0a72\u0a73\u0a74", + + /** + * National Language Identifier: 0x0B + * A.3.11 Tamil National Language Locking Shift Table + */ + // 01.....2.....3.....4.....5.....6.....7.....8.....9A.BCD.E.....F..... + " \u0b82\u0b83\u0b85\u0b86\u0b87\u0b88\u0b89\u0b8a \n \r\u0b8e\u0b8f" + // 0.....12.....3.....4.....5.....6789.....A.....B.....CD.....EF..... + + "\u0b90 \u0b92\u0b93\u0b94\u0b95 \u0b99\u0b9a\uffff \u0b9c \u0b9e" + // 012.....3456.....7.....89ABCDEF..... + + " !\u0b9f \u0ba3\u0ba4)( , .\u0ba8" + // 0123456789ABC.....D.....EF + + "0123456789:;\u0ba9\u0baa ?" + // 012.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....EF + + " \u0bae\u0baf\u0bb0\u0bb1\u0bb2\u0bb3\u0bb4\u0bb5\u0bb6\u0bb7\u0bb8\u0bb9 " + // 0.....1.....2.....3.....4.....5678.....9.....A.....BC.....D.....E.....F..... + + "\u0bbe\u0bbf\u0bc0\u0bc1\u0bc2 \u0bc6\u0bc7\u0bc8 \u0bca\u0bcb\u0bcc\u0bcd" + // 0.....123456789ABCDEF + + "\u0bd0abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u0bd7\u0bf0\u0bf1\u0bf2\u0bf9", + + /** + * National Language Identifier: 0x0C + * A.3.12 Telugu National Language Locking Shift Table + */ + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....CD.E.....F..... + "\u0c01\u0c02\u0c03\u0c05\u0c06\u0c07\u0c08\u0c09\u0c0a\u0c0b\n\u0c0c \r\u0c0e\u0c0f" + // 0.....12.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0c10 \u0c12\u0c13\u0c14\u0c15\u0c16\u0c17\u0c18\u0c19\u0c1a\uffff\u0c1b\u0c1c\u0c1d\u0c1e" + // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF..... + + " !\u0c1f\u0c20\u0c21\u0c22\u0c23\u0c24)(\u0c25\u0c26,\u0c27.\u0c28" + // 0123456789ABCD.....E.....F + + "0123456789:; \u0c2a\u0c2b?" + // 0.....1.....2.....3.....4.....5.....6.....7.....89.....A.....B.....C.....D.....EF..... + + "\u0c2c\u0c2d\u0c2e\u0c2f\u0c30\u0c31\u0c32\u0c33 \u0c35\u0c36\u0c37\u0c38\u0c39 \u0c3d" + // 0.....1.....2.....3.....4.....5.....6.....78.....9.....A.....BC.....D.....E.....F..... + + "\u0c3e\u0c3f\u0c40\u0c41\u0c42\u0c43\u0c44 \u0c46\u0c47\u0c48 \u0c4a\u0c4b\u0c4c\u0c4d" + // 0.....123456789ABCDEF + + "\u0c55abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u0c56\u0c60\u0c61\u0c62\u0c63", + + /** + * National Language Identifier: 0x0D + * A.3.13 Urdu National Language Locking Shift Table + */ + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F..... + "\u0627\u0622\u0628\u067b\u0680\u067e\u06a6\u062a\u06c2\u067f\n\u0679\u067d\r\u067a\u067c" + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u062b\u062c\u0681\u0684\u0683\u0685\u0686\u0687\u062d\u062e\u062f\uffff\u068c\u0688\u0689\u068a" + // 012.....3.....4.....5.....6.....7.....89A.....B.....CD.....EF..... + + " !\u068f\u068d\u0630\u0631\u0691\u0693)(\u0699\u0632,\u0696.\u0698" + // 0123456789ABC.....D.....E.....F + + "0123456789:;\u069a\u0633\u0634?" + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u0635\u0636\u0637\u0638\u0639\u0641\u0642\u06a9\u06aa\u06ab\u06af\u06b3\u06b1\u0644\u0645\u0646" + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F..... + + "\u06ba\u06bb\u06bc\u0648\u06c4\u06d5\u06c1\u06be\u0621\u06cc\u06d0\u06d2\u064d\u0650\u064f\u0657" + // 0.....123456789ABCDEF + + "\u0654abcdefghijklmno" + // 0123456789AB.....C.....D.....E.....F..... + + "pqrstuvwxyz\u0655\u0651\u0653\u0656\u0670" +]; + +// National Language Single Shift Tables, see 3GPP TS 23.038 +this.PDU_NL_SINGLE_SHIFT_TABLES = [ + /** + * National Language Identifier: 0x00 + * 6.2.1.1 GSM 7 bit default alphabet extension table + */ + // 0123456789A.....BCD.....EF + " \u000c \ufffe " + // 0123456789AB.....CDEF + + " ^ \uffff " + // 0123456789ABCDEF. + + " {} \\" + // 0123456789ABCDEF + + " [~] " + // 0123456789ABCDEF + + "| " + // 0123456789ABCDEF + + " " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x01 + * A.2.1 Turkish National Language Single Shift Table + */ + // 0123456789A.....BCD.....EF + " \u000c \ufffe " + // 0123456789AB.....CDEF + + " ^ \uffff " + // 0123456789ABCDEF. + + " {} \\" + // 0123456789ABCDEF + + " [~] " + // 01234567.....89.....ABCDEF + + "| \u011e \u0130 " + // 0123.....456789ABCDEF + + " \u015e " + // 0123.....45.....67.....89.....ABCDEF + + " \u00e7 \u20ac \u011f \u0131 " + // 0123.....456789ABCDEF + + " \u015f ", + + /** + * National Language Identifier: 0x02 + * A.2.2 Spanish National Language Single Shift Table + */ + // 0123456789.....A.....BCD.....EF + " \u00e7\u000c \ufffe " + // 0123456789AB.....CDEF + + " ^ \uffff " + // 0123456789ABCDEF. + + " {} \\" + // 0123456789ABCDEF + + " [~] " + // 01.....23456789.....ABCDEF..... + + "|\u00c1 \u00cd \u00d3" + // 012345.....6789ABCDEF + + " \u00da " + // 01.....2345.....6789.....ABCDEF..... + + " \u00e1 \u20ac \u00ed \u00f3" + // 012345.....6789ABCDEF + + " \u00fa ", + + /** + * National Language Identifier: 0x03 + * A.2.3 Portuguese National Language Single Shift Table + */ + // 012345.....6789.....A.....B.....C.....D.....E.....F..... + " \u00ea \u00e7\u000c\u00d4\u00f4\ufffe\u00c1\u00e1" + // 012.....3.....45.....6.....7.....8.....9.....AB.....CDEF..... + + " \u03a6\u0393^\u03a9\u03a0\u03a8\u03a3\u0398 \uffff \u00ca" + // 0123456789ABCDEF. + + " {} \\" + // 0123456789ABCDEF + + " [~] " + // 01.....23456789.....ABCDEF..... + + "|\u00c0 \u00cd \u00d3" + // 012345.....6789AB.....C.....DEF + + " \u00da \u00c3\u00d5 " + // 01.....2345.....6789.....ABCDEF..... + + " \u00c2 \u20ac \u00ed \u00f3" + // 012345.....6789AB.....C.....DEF..... + + " \u00fa \u00e3\u00f5 \u00e2", + + /** + * National Language Identifier: 0x04 + * A.2.4 Bengali National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789.....A.....B.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#*\u09e6\u09e7\uffff\u09e8\u09e9\u09ea\u09eb" + // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....D.....E.....F. + + "\u09ec\u09ed\u09ee\u09ef\u09df\u09e0\u09e1\u09e2{}\u09e3\u09f2\u09f3\u09f4\u09f5\\" + // 0.....1.....2.....3.....4.....56789ABCDEF + + "\u09f6\u09f7\u09f8\u09f9\u09fa [~] " + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x05 + * A.2.5 Gujarati National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789.....A.....B.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#*\u0964\u0965\uffff\u0ae6\u0ae7\u0ae8\u0ae9" + // 0.....1.....2.....3.....4.....5.....6789ABCDEF. + + "\u0aea\u0aeb\u0aec\u0aed\u0aee\u0aef {} \\" + // 0123456789ABCDEF + + " [~] " + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x06 + * A.2.6 Hindi National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789.....A.....B.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#*\u0964\u0965\uffff\u0966\u0967\u0968\u0969" + // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....D.....E.....F. + + "\u096a\u096b\u096c\u096d\u096e\u096f\u0951\u0952{}\u0953\u0954\u0958\u0959\u095a\\" + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....BCDEF + + "\u095b\u095c\u095d\u095e\u095f\u0960\u0961\u0962\u0963\u0970\u0971 [~] " + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x07 + * A.2.7 Kannada National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789.....A.....B.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#*\u0964\u0965\uffff\u0ce6\u0ce7\u0ce8\u0ce9" + // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....BCDEF. + + "\u0cea\u0ceb\u0cec\u0ced\u0cee\u0cef\u0cde\u0cf1{}\u0cf2 \\" + // 0123456789ABCDEF + + " [~] " + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x08 + * A.2.8 Malayalam National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789.....A.....B.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#*\u0964\u0965\uffff\u0d66\u0d67\u0d68\u0d69" + // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....D.....E.....F. + + "\u0d6a\u0d6b\u0d6c\u0d6d\u0d6e\u0d6f\u0d70\u0d71{}\u0d72\u0d73\u0d74\u0d75\u0d7a\\" + // 0.....1.....2.....3.....4.....56789ABCDEF + + "\u0d7b\u0d7c\u0d7d\u0d7e\u0d7f [~] " + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x09 + * A.2.9 Oriya National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789.....A.....B.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#*\u0964\u0965\uffff\u0b66\u0b67\u0b68\u0b69" + // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....DEF. + + "\u0b6a\u0b6b\u0b6c\u0b6d\u0b6e\u0b6f\u0b5c\u0b5d{}\u0b5f\u0b70\u0b71 \\" + // 0123456789ABCDEF + + " [~] " + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x0A + * A.2.10 Punjabi National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789.....A.....B.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#*\u0964\u0965\uffff\u0a66\u0a67\u0a68\u0a69" + // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....D.....EF. + + "\u0a6a\u0a6b\u0a6c\u0a6d\u0a6e\u0a6f\u0a59\u0a5a{}\u0a5b\u0a5c\u0a5e\u0a75 \\" + // 0123456789ABCDEF + + " [~] " + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x0B + * A.2.11 Tamil National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789.....A.....B.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#*\u0964\u0965\uffff\u0be6\u0be7\u0be8\u0be9" + // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....D.....E.....F. + + "\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef\u0bf3\u0bf4{}\u0bf5\u0bf6\u0bf7\u0bf8\u0bfa\\" + // 0123456789ABCDEF + + " [~] " + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x0C + * A.2.12 Telugu National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789AB.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#* \uffff\u0c66\u0c67\u0c68\u0c69" + // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....D.....E.....F. + + "\u0c6a\u0c6b\u0c6c\u0c6d\u0c6e\u0c6f\u0c58\u0c59{}\u0c78\u0c79\u0c7a\u0c7b\u0c7c\\" + // 0.....1.....2.....3456789ABCDEF + + "\u0c7d\u0c7e\u0c7f [~] " + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " ", + + /** + * National Language Identifier: 0x0D + * A.2.13 Urdu National Language Single Shift Table + */ + // 01.....23.....4.....5.6.....789A.....BCD.....EF + "@\u00a3$\u00a5\u00bf\"\u00a4%&'\u000c*+\ufffe-/" + // 0123.....45.....6789.....A.....B.....C.....D.....E.....F..... + + "<=>\u00a1^\u00a1_#*\u0600\u0601\uffff\u06f0\u06f1\u06f2\u06f3" + // 0.....1.....2.....3.....4.....5.....6.....7.....89A.....B.....C.....D.....E.....F. + + "\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9\u060c\u060d{}\u060e\u060f\u0610\u0611\u0612\\" + // 0.....1.....2.....3.....4.....5.....6.....7.....8.....9.....A.....B.....CDEF..... + + "\u0613\u0614\u061b\u061f\u0640\u0652\u0658\u066b\u066c\u0672\u0673\u06cd[~]\u06d4" + // 0123456789ABCDEF + + "|ABCDEFGHIJKLMNO" + // 0123456789ABCDEF + + "PQRSTUVWXYZ " + // 012345.....6789ABCDEF + + " \u20ac " + // 0123456789ABCDEF + + " " +]; + +// Special SMS Message Indication constants +this.PDU_MWI_STORE_TYPE_BIT = 0x80; +this.PDU_MWI_STORE_TYPE_DISCARD = 0x00; +this.PDU_MWI_STORE_TYPE_STORE = 0x80; + +this.GSM_SMS_STRICT_7BIT_CHARMAP = { +//"\u0024": "\u0024", // "$" => "$", already in default alphabet +//"\u00a5": "\u00a5", // "¥" => "¥", already in default alphabet + "\u00c0": "\u0041", // "À" => "A" + "\u00c1": "\u0041", // "Á" => "A" + "\u00c2": "\u0041", // "Â" => "A" + "\u00c3": "\u0041", // "Ã" => "A" +//"\u00c4": "\u00c4", // "Ä" => "Ä", already in default alphabet +//"\u00c5": "\u00c5", // "Å" => "Å", already in default alphabet +//"\u00c6": "\u00c6", // "Æ" => "Æ", already in default alphabet +//"\u00c7": "\u00c7", // "Ç" => "Ç", already in default alphabet + "\u00c8": "\u0045", // "È" => "E" +//"\u00c9": "\u00c9", // "É" => "É", already in default alphabet + "\u00ca": "\u0045", // "Ê" => "E" + "\u00cb": "\u0045", // "Ë" => "E" + "\u00cc": "\u0049", // "Ì" => "I" + "\u00cd": "\u0049", // "Í" => "I" + "\u00ce": "\u0049", // "Î" => "I" + "\u00cf": "\u0049", // "Ï" => "I" +//"\u00d1": "\u00d1", // "Ñ" => "Ñ", already in default alphabet + "\u00d2": "\u004f", // "Ò" => "O" + "\u00d3": "\u004f", // "Ó" => "O" + "\u00d4": "\u004f", // "Ô" => "O" + "\u00d5": "\u004f", // "Õ" => "O" +//"\u00d6": "\u00d6", // "Ö" => "Ö", already in default alphabet + "\u00d9": "\u0055", // "Ù" => "U" + "\u00da": "\u0055", // "Ú" => "U" + "\u00db": "\u0055", // "Û" => "U" +//"\u00dc": "\u00dc", // "Ü" => "Ü", already in default alphabet +//"\u00df": "\u00df", // "ß" => "ß", already in default alphabet +//"\u00e0": "\u00e0", // "à" => "à", already in default alphabet + "\u00e1": "\u0061", // "á" => "a" + "\u00e2": "\u0061", // "â" => "a" + "\u00e3": "\u0061", // "ã" => "a" +//"\u00e4": "\u00e4", // "ä" => "ä", already in default alphabet +//"\u00e5": "\u00e5", // "å" => "å", already in default alphabet +//"\u00e6": "\u00e6", // "æ" => "æ", already in default alphabet + "\u00e7": "\u00c7", // "ç" => "Ç" +//"\u00e8": "\u00e8", // "è" => "è", already in default alphabet +//"\u00e9": "\u00e9", // "é" => "é", already in default alphabet + "\u00ea": "\u0065", // "ê" => "e" + "\u00eb": "\u0065", // "ë" => "e" +//"\u00ec": "\u00ec", // "ì" => "ì", already in default alphabet + "\u00ed": "\u0069", // "í" => "i" + "\u00ee": "\u0069", // "î" => "i" + "\u00ef": "\u0069", // "ï" => "i" +//"\u00f1": "\u00f1", // "ñ" => "ñ", already in default alphabet +//"\u00f2": "\u00f2", // "ò" => "ò", already in default alphabet + "\u00f3": "\u006f", // "ó" => "o" + "\u00f4": "\u006f", // "ô" => "o" + "\u00f5": "\u006f", // "õ" => "o" +//"\u00f6": "\u00f6", // "ö" => "ö", already in default alphabet +//"\u00f8": "\u00f8", // "ø" => "ø", already in default alphabet +//"\u00f9": "\u00f9", // "ù" => "ù", already in default alphabet + "\u00fa": "\u0075", // "ú" => "u" + "\u00fb": "\u0075", // "û" => "u" +//"\u00fc": "\u00fc", // "ü" => "ü", already in default alphabet + "\u00fe": "\u0074", // "þ" => "t" + "\u0100": "\u0041", // "Ā" => "A" + "\u0101": "\u0061", // "ā" => "a" + "\u0106": "\u0043", // "Ć" => "C" + "\u0107": "\u0063", // "ć" => "c" + "\u010c": "\u0043", // "Č" => "C" + "\u010d": "\u0063", // "č" => "c" + "\u010f": "\u0064", // "ď" => "d" + "\u0110": "\u0044", // "Đ" => "D" + "\u0111": "\u0064", // "đ" => "d" + "\u0112": "\u0045", // "Ē" => "E" + "\u0113": "\u0065", // "ē" => "e" + "\u0118": "\u0045", // "Ę" => "E" + "\u0119": "\u0065", // "ę" => "e" + "\u0128": "\u0049", // "Ĩ" => "I" + "\u0129": "\u0069", // "ĩ" => "i" + "\u012a": "\u0049", // "Ī" => "I" + "\u012b": "\u0069", // "ī" => "i" + "\u012e": "\u0049", // "Į" => "I" + "\u012f": "\u0069", // "į" => "i" + "\u0141": "\u004c", // "Ł" => "L" + "\u0142": "\u006c", // "ł" => "l" + "\u0143": "\u004e", // "Ń" => "N" + "\u0144": "\u006e", // "ń" => "n" + "\u0147": "\u004e", // "Ň" => "N" + "\u0148": "\u006e", // "ň" => "n" + "\u014c": "\u004f", // "Ō" => "O" + "\u014d": "\u006f", // "ō" => "o" + "\u0152": "\u004f", // "Œ" => "O" + "\u0153": "\u006f", // "œ" => "o" + "\u0158": "\u0052", // "Ř" => "R" + "\u0159": "\u0072", // "ř" => "r" + "\u0160": "\u0053", // "Š" => "S" + "\u0161": "\u0073", // "š" => "s" + "\u0165": "\u0074", // "ť" => "t" + "\u0168": "\u0055", // "Ū" => "U" + "\u0169": "\u0075", // "ū" => "u" + "\u016a": "\u0055", // "Ū" => "U" + "\u016b": "\u0075", // "ū" => "u" + "\u0178": "\u0059", // "Ÿ" => "Y" + "\u0179": "\u005a", // "Ź" => "Z" + "\u017a": "\u007a", // "ź" => "z" + "\u017b": "\u005a", // "Ż" => "Z" + "\u017c": "\u007a", // "ż" => "z" + "\u017d": "\u005a", // "Ž" => "Z" + "\u017e": "\u007a", // "ž" => "z" + "\u025b": "\u0045", // "ɛ" => "E" +//"\u0398": "\u0398", // "Θ" => "Θ", already in default alphabet + "\u1e7c": "\u0056", // "Ṽ" => "V" + "\u1e7d": "\u0076", // "ṽ" => "v" + "\u1ebc": "\u0045", // "Ẽ" => "E" + "\u1ebd": "\u0065", // "ẽ" => "e" + "\u1ef8": "\u0059", // "Ỹ" => "Y" + "\u1ef9": "\u0079", // "ỹ" => "y" + "\u20a4": "\u00a3", // "₤" => "£" +//"\u20ac": "\u20ac", // "€" => "€", already in default alphabet +}; + +this.RADIOTECH_FAMILY_3GPP = 1; // GSM, WCDMA, LTE +this.RADIOTECH_FAMILY_3GPP2 = 2; // CDMA, EVDO + +this.DATACALL_RADIOTECHNOLOGY_CDMA = 0; +this.DATACALL_RADIOTECHNOLOGY_GSM = 1; + +this.DATACALL_AUTH_NONE = 0; +this.DATACALL_AUTH_PAP = 1; +this.DATACALL_AUTH_CHAP = 2; +this.DATACALL_AUTH_PAP_OR_CHAP = 3; + +this.GECKO_DATACALL_AUTH_NONE = "none"; +this.GECKO_DATACALL_AUTH_PAP = "pap"; +this.GECKO_DATACALL_AUTH_CHAP = "chap"; +this.GECKO_DATACALL_AUTH_PAP_OR_CHAP = "papOrChap"; +this.GECKO_DATACALL_AUTH_DEFAULT = GECKO_DATACALL_AUTH_PAP_OR_CHAP; +this.RIL_DATACALL_AUTH_TO_GECKO = [ + GECKO_DATACALL_AUTH_NONE, // DATACALL_AUTH_NONE + GECKO_DATACALL_AUTH_PAP, // DATACALL_AUTH_PAP + GECKO_DATACALL_AUTH_CHAP, // DATACALL_AUTH_CHAP + GECKO_DATACALL_AUTH_PAP_OR_CHAP // DATACALL_AUTH_PAP_OR_CHAP +]; + +this.GECKO_DATACALL_PDP_TYPE_IP = "IP"; +this.GECKO_DATACALL_PDP_TYPE_IPV4V6 = "IPV4V6"; +this.GECKO_DATACALL_PDP_TYPE_IPV6 = "IPV6"; +this.GECKO_DATACALL_PDP_TYPE_DEFAULT = GECKO_DATACALL_PDP_TYPE_IP; +this.RIL_DATACALL_PDP_TYPES = [ + GECKO_DATACALL_PDP_TYPE_IP, + GECKO_DATACALL_PDP_TYPE_IPV4V6, + GECKO_DATACALL_PDP_TYPE_IPV6, +]; + +this.DATACALL_PROFILE_DEFAULT = 0; +this.DATACALL_PROFILE_TETHERED = 1; +this.DATACALL_PROFILE_OEM_BASE = 1000; + +this.DATACALL_DEACTIVATE_NO_REASON = 0; +this.DATACALL_DEACTIVATE_RADIO_SHUTDOWN = 1; + +this.DATACALL_ACTIVE_UNKNOWN = -1; +this.DATACALL_INACTIVE = 0; +this.DATACALL_ACTIVE_DOWN = 1; +this.DATACALL_ACTIVE_UP = 2; + +this.DATACALL_FAIL_NONE = 0; +this.DATACALL_FAIL_OPERATOR_BARRED = 0x08; +this.DATACALL_FAIL_INSUFFICIENT_RESOURCES = 0x1A; +this.DATACALL_FAIL_MISSING_UKNOWN_APN = 0x1B; +this.DATACALL_FAIL_UNKNOWN_PDP_ADDRESS_TYPE = 0x1C; +this.DATACALL_FAIL_USER_AUTHENTICATION = 0x1D; +this.DATACALL_FAIL_ACTIVATION_REJECT_GGSN = 0x1E; +this.DATACALL_FAIL_ACTIVATION_REJECT_UNSPECIFIED = 0x1F; +this.DATACALL_FAIL_SERVICE_OPTION_NOT_SUPPORTED = 0x20; +this.DATACALL_FAIL_SERVICE_OPTION_NOT_SUBSCRIBED = 0x21; +this.DATACALL_FAIL_SERVICE_OPTION_OUT_OF_ORDER = 0x22; +this.DATACALL_FAIL_NSAPI_IN_USE = 0x23; +this.DATACALL_FAIL_ONLY_IPV4_ALLOWED = 0x32; +this.DATACALL_FAIL_ONLY_IPV6_ALLOWED = 0x33; +this.DATACALL_FAIL_ONLY_SINGLE_BEARER_ALLOWED = 0x34; +this.DATACALL_FAIL_PROTOCOL_ERRORS = 0x6F; +this.DATACALL_FAIL_VOICE_REGISTRATION_FAIL = -1; +this.DATACALL_FAIL_DATA_REGISTRATION_FAIL = -2; +this.DATACALL_FAIL_SIGNAL_LOST = -3; +this.DATACALL_FAIL_PREF_RADIO_TECH_CHANGED = -4; +this.DATACALL_FAIL_RADIO_POWER_OFF = -5; +this.DATACALL_FAIL_TETHERED_CALL_ACTIVE = -6; +this.DATACALL_FAIL_ERROR_UNSPECIFIED = 0xffff; + +// Keep consistent with nsINetworkManager.NETWORK_STATE_*. +this.GECKO_NETWORK_STATE_UNKNOWN = -1; +this.GECKO_NETWORK_STATE_CONNECTING = 0; +this.GECKO_NETWORK_STATE_CONNECTED = 1; +this.GECKO_NETWORK_STATE_DISCONNECTING = 2; +this.GECKO_NETWORK_STATE_DISCONNECTED = 3; + +// 3GPP 24.008 Annex H. +this.CALL_FAIL_UNOBTAINABLE_NUMBER = 1; +this.CALL_FAIL_NO_ROUTE_TO_DESTINATION = 3; +this.CALL_FAIL_CHANNEL_UNACCEPTABLE = 6; +this.CALL_FAIL_OPERATOR_DETERMINED_BARRING = 8; +this.CALL_FAIL_NORMAL = 16; +this.CALL_FAIL_BUSY = 17; +this.CALL_FAIL_NO_USER_RESPONDING = 18; +this.CALL_FAIL_USER_ALERTING = 19; +this.CALL_FAIL_CALL_REJECTED = 21; +this.CALL_FAIL_NUMBER_CHANGED = 22; +this.CALL_FAIL_CALL_REJECTED_DESTINATION_FEATURE = 24; +this.CALL_FAIL_CALL_PRE_EMPTION = 25; +this.CALL_FAIL_DEST_OUT_OF_ORDER = 27; +this.CALL_FAIL_INVALID_FORMAT = 28; +this.CALL_FAIL_FACILITY_REJECTED = 29; +this.CALL_FAIL_RESPONSE_TO_STATUS_ENQUIRY = 30; +this.CALL_FAIL_NORMAL_UNSPECIFIED = 31; +this.CALL_FAIL_CONGESTION = 34; +this.CALL_FAIL_NETWORK_OUT_OF_ORDER = 38; +this.CALL_FAIL_NETWORK_TEMP_FAILURE = 41; +this.CALL_FAIL_SWITCHING_EQUIP_CONGESTION = 42; +this.CALL_FAIL_ACCESS_INFO_DISCARDED = 43; +this.CALL_FAIL_REQUESTED_CHANNEL_NOT_AVAILABLE = 44; +this.CALL_FAIL_RESOURCE_UNAVAILABLE = 47; +this.CALL_FAIL_QOS_UNAVAILABLE = 49; +this.CALL_FAIL_REQUESTED_FACILITY_NOT_SUBSCRIBED = 50; +this.CALL_FAIL_INCOMING_CALLS_BARRED_WITHIN_CUG = 55; +this.CALL_FAIL_BEARER_CAPABILITY_NOT_AUTHORIZED = 57; +this.CALL_FAIL_BEARER_CAPABILITY_NOT_AVAILABLE = 58; +this.CALL_FAIL_SERVICE_NOT_AVAILABLE = 63; +this.CALL_FAIL_BEARER_NOT_IMPLEMENTED = 65; +this.CALL_FAIL_ACM_LIMIT_EXCEEDED = 68; +this.CALL_FAIL_REQUESTED_FACILITY_NOT_IMPLEMENTED = 69; +this.CALL_FAIL_UNRESTRICTED_BEARER_NOT_AVAILABLE = 70; +this.CALL_FAIL_SERVICE_NOT_IMPLEMENTED = 79; +this.CALL_FAIL_INVALID_TRANSACTION_ID = 81; +this.CALL_FAIL_USER_NOT_CUG_MEMBER = 87; +this.CALL_FAIL_INCOMPATIBLE_DESTINATION = 88; +this.CALL_FAIL_INVALID_TRANSIT_NETWORK_SELECTION = 91; +this.CALL_FAIL_SEMANTICALLY_INCORRECT_MESSAGE = 95; +this.CALL_FAIL_INVALID_MANDATORY_INFO = 96; +this.CALL_FAIL_MESSAGE_TYPE_NOT_IMPLEMENTED = 97; +this.CALL_FAIL_MESSAGE_TYPE_INCOMPATIBLE_PROTOCOL_STATE = 98; +this.CALL_FAIL_INFO_ELEMENT_NOT_IMPLEMENTED = 99; +this.CALL_FAIL_CONDITIONAL_IE_ERROR = 100; +this.CALL_FAIL_MESSAGE_INCOMPABITLE_PROTOCOL_STATE = 101; +this.CALL_FAIL_RECOVERY_ON_TIMER_EXPIRY = 102; +this.CALL_FAIL_PROTOCOL_ERROR = 111; +this.CALL_FAIL_INTERWORKING = 127; +// AOSP ril.h +this.CALL_FAIL_CALL_BARRED = 240; +this.CALL_FAIL_FDN_BLOCKED = 241; +this.CALL_FAIL_IMSI_UNKNOWN_IN_VLR = 242; +this.CALL_FAIL_IMEI_NOT_ACCEPTED = 243; +this.CALL_FAIL_DIAL_MODIFIED_TO_USSD = 244; // STK Call Control +this.CALL_FAIL_DIAL_MODIFIED_TO_SS = 245; +this.CALL_FAIL_DIAL_MODIFIED_TO_DIAL = 246; +this.CALL_FAIL_CDMA_LOCKED_UNTIL_POWER_CYCLE = 1000; +this.CALL_FAIL_CDMA_DROP = 1001; +this.CALL_FAIL_CDMA_INTERCEPT = 1002; +this.CALL_FAIL_CDMA_REORDER = 1003; +this.CALL_FAIL_CDMA_SO_REJECT = 1004; +this.CALL_FAIL_CDMA_RETRY_ORDER = 1005; +this.CALL_FAIL_CDMA_ACCESS_FAILURE = 1006; +this.CALL_FAIL_CDMA_PREEMPTED = 1007; +this.CALL_FAIL_CDMA_NOT_EMERGENCY = 1008; // For non-emergency number dialed + // during emergency callback mode +this.CALL_FAIL_CDMA_ACCESS_BLOCKED = 1009; +this.CALL_FAIL_ERROR_UNSPECIFIED = 0xffff; + +// See nsIMobileConnection::MOBILE_RADIO_STATE_* +this.GECKO_RADIOSTATE_UNKNOWN = -1; +this.GECKO_RADIOSTATE_ENABLED = 0; +this.GECKO_RADIOSTATE_DISABLED = 1; + +// Only used in ril_worker.js +this.GECKO_CARDSTATE_UNINITIALIZED = 4294967294; // UINT32_MAX - 1 +// See nsIIcc::CARD_STATE_* +this.GECKO_CARDSTATE_UNDETECTED = 4294967295; // UINT32_MAX +this.GECKO_CARDSTATE_UNKNOWN = 0; +this.GECKO_CARDSTATE_READY = 1; +this.GECKO_CARDSTATE_PIN_REQUIRED = 2; +this.GECKO_CARDSTATE_PUK_REQUIRED = 3; +this.GECKO_CARDSTATE_PERMANENT_BLOCKED = 4; +this.GECKO_CARDSTATE_PERSONALIZATION_IN_PROGRESS = 5; +this.GECKO_CARDSTATE_PERSONALIZATION_READY = 6; +this.GECKO_CARDSTATE_NETWORK_LOCKED = 7; +this.GECKO_CARDSTATE_NETWORK_SUBSET_LOCKED = 8; +this.GECKO_CARDSTATE_CORPORATE_LOCKED = 9; +this.GECKO_CARDSTATE_SERVICE_PROVIDER_LOCKED = 10; +this.GECKO_CARDSTATE_SIM_LOCKED = 11; +this.GECKO_CARDSTATE_NETWORK_PUK_REQUIRED = 12; +this.GECKO_CARDSTATE_NETWORK_SUBSET_PUK_REQUIRED = 13; +this.GECKO_CARDSTATE_CORPORATE_PUK_REQUIRED = 14; +this.GECKO_CARDSTATE_SERVICE_PROVIDER_PUK_REQUIRED = 15; +this.GECKO_CARDSTATE_SIM_PUK_REQUIRED = 16; +this.GECKO_CARDSTATE_NETWORK1_LOCKED = 17; +this.GECKO_CARDSTATE_NETWORK2_LOCKED = 18; +this.GECKO_CARDSTATE_HRPD_NETWORK_LOCKED = 19; +this.GECKO_CARDSTATE_RUIM_CORPORATE_LOCKED = 20; +this.GECKO_CARDSTATE_RUIM_SERVICE_PROVIDER_LOCKED = 21; +this.GECKO_CARDSTATE_RUIM_LOCKED = 22; +this.GECKO_CARDSTATE_NETWORK1_PUK_REQUIRED = 23; +this.GECKO_CARDSTATE_NETWORK2_PUK_REQUIRED = 24; +this.GECKO_CARDSTATE_HRPD_NETWORK_PUK_REQUIRED = 25; +this.GECKO_CARDSTATE_RUIM_CORPORATE_PUK_REQUIRED = 26; +this.GECKO_CARDSTATE_RUIM_SERVICE_PROVIDER_PUK_REQUIRED = 27; +this.GECKO_CARDSTATE_RUIM_PUK_REQUIRED = 28; +this.GECKO_CARDSTATE_ILLEGAL = 29; + +// See nsIIcc::CARD_LOCK_TYPE_* +this.GECKO_CARDLOCK_PIN = 0; +this.GECKO_CARDLOCK_PIN2 = 1; +this.GECKO_CARDLOCK_PUK = 2; +this.GECKO_CARDLOCK_PUK2 = 3; +this.GECKO_CARDLOCK_NCK = 4; +this.GECKO_CARDLOCK_NSCK = 5; +this.GECKO_CARDLOCK_NCK1 = 6; +this.GECKO_CARDLOCK_NCK2 = 7; +this.GECKO_CARDLOCK_HNCK = 8; +this.GECKO_CARDLOCK_CCK = 9; +this.GECKO_CARDLOCK_SPCK = 10; +this.GECKO_CARDLOCK_PCK = 11; +this.GECKO_CARDLOCK_RCCK = 12; +this.GECKO_CARDLOCK_RSPCK = 13; +this.GECKO_CARDLOCK_NCK_PUK = 14; +this.GECKO_CARDLOCK_NSCK_PUK = 15; +this.GECKO_CARDLOCK_NCK1_PUK = 16; +this.GECKO_CARDLOCK_NCK2_PUK = 17; +this.GECKO_CARDLOCK_HNCK_PUK = 18; +this.GECKO_CARDLOCK_CCK_PUK = 19; +this.GECKO_CARDLOCK_SPCK_PUK = 20; +this.GECKO_CARDLOCK_PCK_PUK = 21; +this.GECKO_CARDLOCK_RCCK_PUK = 22; +this.GECKO_CARDLOCK_RSPCK_PUK = 23; +this.GECKO_CARDLOCK_FDN = 24; + +this.GECKO_CARDLOCK_TO_FACILITY = {}; +GECKO_CARDLOCK_TO_FACILITY[GECKO_CARDLOCK_PIN] = ICC_CB_FACILITY_SIM; +GECKO_CARDLOCK_TO_FACILITY[GECKO_CARDLOCK_FDN] = ICC_CB_FACILITY_FDN; + +this.GECKO_CARDLOCK_TO_SEL_CODE = {}; +GECKO_CARDLOCK_TO_SEL_CODE[GECKO_CARDLOCK_PIN] = ICC_SEL_CODE_SIM_PIN; +GECKO_CARDLOCK_TO_SEL_CODE[GECKO_CARDLOCK_PIN2] = ICC_SEL_CODE_SIM_PIN2; +GECKO_CARDLOCK_TO_SEL_CODE[GECKO_CARDLOCK_PUK] = ICC_SEL_CODE_SIM_PUK; +GECKO_CARDLOCK_TO_SEL_CODE[GECKO_CARDLOCK_PUK2] = ICC_SEL_CODE_SIM_PUK2; +GECKO_CARDLOCK_TO_SEL_CODE[GECKO_CARDLOCK_NCK] = ICC_SEL_CODE_PH_NET_PIN; +GECKO_CARDLOCK_TO_SEL_CODE[GECKO_CARDLOCK_NSCK] = ICC_SEL_CODE_PH_NETSUB_PIN; +GECKO_CARDLOCK_TO_SEL_CODE[GECKO_CARDLOCK_CCK] = ICC_SEL_CODE_PH_CORP_PIN; +GECKO_CARDLOCK_TO_SEL_CODE[GECKO_CARDLOCK_SPCK] = ICC_SEL_CODE_PH_SP_PIN; +// TODO: Bug 1116072: identify the mapping between RIL_PERSOSUBSTATE_SIM_SIM @ +// ril.h and TS 27.007, clause 8.65 for GECKO_CARDLOCK_PCK. + +// See nsIIcc::CARD_CONTACT_TYPE_* +this.GECKO_CARDCONTACT_TYPE_ADN = 0; +this.GECKO_CARDCONTACT_TYPE_FDN = 1; +this.GECKO_CARDCONTACT_TYPE_SDN = 2; + +// See nsIIcc::CARD_MVNO_TYPE_* +this.GECKO_CARDMVNO_TYPE_IMSI = 0; +this.GECKO_CARDMVNO_TYPE_SPN = 1; +this.GECKO_CARDMVNO_TYPE_GID = 2; + +// See nsIIcc::CARD_SERVICE_* +this.GECKO_CARDSERVICE_FDN = 0; + +// See ril.h RIL_PersoSubstate +this.PERSONSUBSTATE = {}; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_UNKNOWN] = GECKO_CARDSTATE_UNKNOWN; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_IN_PROGRESS] = GECKO_CARDSTATE_PERSONALIZATION_IN_PROGRESS; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_READY] = GECKO_CARDSTATE_PERSONALIZATION_READY; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_NETWORK] = GECKO_CARDSTATE_NETWORK_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_NETWORK_SUBSET] = GECKO_CARDSTATE_NETWORK_SUBSET_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_CORPORATE] = GECKO_CARDSTATE_CORPORATE_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_SERVICE_PROVIDER] = GECKO_CARDSTATE_SERVICE_PROVIDER_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_SIM] = GECKO_CARDSTATE_SIM_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_NETWORK_PUK] = GECKO_CARDSTATE_NETWORK_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK] = GECKO_CARDSTATE_NETWORK_SUBSET_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_CORPORATE_PUK] = GECKO_CARDSTATE_CORPORATE_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK] = GECKO_CARDSTATE_SERVICE_PROVIDER_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_SIM_SIM_PUK] = GECKO_CARDSTATE_SIM_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_NETWORK1] = GECKO_CARDSTATE_NETWORK1_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_NETWORK2] = GECKO_CARDSTATE_NETWORK2_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_HRPD] = GECKO_CARDSTATE_HRPD_NETWORK_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_CORPORATE] = GECKO_CARDSTATE_RUIM_CORPORATE_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_SERVICE_PROVIDER] = GECKO_CARDSTATE_RUIM_SERVICE_PROVIDER_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_RUIM] = GECKO_CARDSTATE_RUIM_LOCKED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_NETWORK1_PUK] = GECKO_CARDSTATE_NETWORK1_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_NETWORK2_PUK] = GECKO_CARDSTATE_NETWORK2_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_HRPD_PUK] = GECKO_CARDSTATE_HRPD_NETWORK_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_CORPORATE_PUK] = GECKO_CARDSTATE_RUIM_CORPORATE_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK] = GECKO_CARDSTATE_RUIM_SERVICE_PROVIDER_PUK_REQUIRED; +PERSONSUBSTATE[CARD_PERSOSUBSTATE_RUIM_RUIM_PUK] = GECKO_CARDSTATE_RUIM_PUK_REQUIRED; + +// See nsIMobileConnection::NETWORK_SELECTION_MODE_* +this.GECKO_NETWORK_SELECTION_UNKNOWN = -1; +this.GECKO_NETWORK_SELECTION_AUTOMATIC = 0; +this.GECKO_NETWORK_SELECTION_MANUAL = 1; + +this.GECKO_MOBILE_CONNECTION_STATE_UNKNOWN = null; +this.GECKO_MOBILE_CONNECTION_STATE_NOTSEARCHING = "notSearching"; +this.GECKO_MOBILE_CONNECTION_STATE_SEARCHING = "searching"; +this.GECKO_MOBILE_CONNECTION_STATE_REGISTERED = "registered"; +this.GECKO_MOBILE_CONNECTION_STATE_DENIED = "denied"; + +this.NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE = {}; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_NOT_SEARCHING] = GECKO_MOBILE_CONNECTION_STATE_NOTSEARCHING; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_REGISTERED_HOME] = GECKO_MOBILE_CONNECTION_STATE_REGISTERED; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_SEARCHING] = GECKO_MOBILE_CONNECTION_STATE_SEARCHING; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_DENIED] = GECKO_MOBILE_CONNECTION_STATE_DENIED; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_UNKNOWN] = GECKO_MOBILE_CONNECTION_STATE_UNKNOWN; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_REGISTERED_ROAMING] = GECKO_MOBILE_CONNECTION_STATE_REGISTERED; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_NOT_SEARCHING_EMERGENCY_CALLS] = GECKO_MOBILE_CONNECTION_STATE_NOTSEARCHING; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_SEARCHING_EMERGENCY_CALLS] = GECKO_MOBILE_CONNECTION_STATE_SEARCHING; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_DENIED_EMERGENCY_CALLS] = GECKO_MOBILE_CONNECTION_STATE_DENIED; +NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[NETWORK_CREG_STATE_UNKNOWN_EMERGENCY_CALLS] = GECKO_MOBILE_CONNECTION_STATE_UNKNOWN; + + +// Should match enum TelephonyCallDisconnectedReason defined in TelephonyCall.webidl +this.GECKO_CALL_ERROR_BAD_NUMBER = "BadNumberError"; +this.GECKO_CALL_ERROR_NO_ROUTE_TO_DESTINATION = "NoRouteToDestinationError"; +this.GECKO_CALL_ERROR_CHANNEL_UNACCEPTABLE = "ChannelUnacceptableError"; +this.GECKO_CALL_ERROR_OPERATOR_DETERMINED_BARRING = "OperatorDeterminedBarringError"; +this.GECKO_CALL_ERROR_NORMAL_CALL_CLEARING = "NormalCallClearingError"; +this.GECKO_CALL_ERROR_BUSY = "BusyError"; +this.GECKO_CALL_ERROR_NO_USER_RESPONDING = "NoUserRespondingError"; +this.GECKO_CALL_ERROR_USER_ALERTING = "UserAlertingNoAnswerError"; +this.GECKO_CALL_ERROR_REJECTED = "CallRejectedError"; +this.GECKO_CALL_ERROR_NUMBER_CHANGED = "NumberChangedError"; +this.GECKO_CALL_ERROR_REJECTED_DETINATION_FEATURE = "CallRejectedDestinationFeatureError"; +this.GECKO_CALL_ERROR_PRE_EMPTION = "PreEmptionError"; +this.GECKO_CALL_ERROR_DEST_OUT_OF_ORDER = "DestinationOutOfOrderError"; +this.GECKO_CALL_ERROR_INVALID_NUMBER_FORMAT = "InvalidNumberFormatError"; +this.GECKO_CALL_ERROR_FACILITY_REJECTED = "FacilityRejectedError"; +this.GECKO_CALL_ERROR_RESPONSE_TO_STATUS_ENQUIRY = "ResponseToStatusEnquiryError"; +this.GECKO_CALL_ERROR_CONGESTION = "CongestionError"; +this.GECKO_CALL_ERROR_NETWORK_OUT_OF_ORDER = "NetworkOutOfOrderError"; +this.GECKO_CALL_ERROR_NETWORK_TEMP_FAILURE = "NetworkTempFailureError"; +this.GECKO_CALL_ERROR_SWITCHING_EQUIP_CONGESTION = "SwitchingEquipCongestionError"; +this.GECKO_CALL_ERROR_ACCESS_INFO_DISCARDED = "AccessInfoDiscardedError"; +this.GECKO_CALL_ERROR_REQUESTED_CHANNEL_NOT_AVAILABLE = "RequestedChannelNotAvailableError"; +this.GECKO_CALL_ERROR_RESOURCE_UNAVAILABLE = "ResourceUnavailableError"; +this.GECKO_CALL_ERROR_QOS_UNAVAILABLE = "QosUnavailableError"; +this.GECKO_CALL_ERROR_REQUESTED_FACILITY_NOT_SUBSCRIBED = "RequestedFacilityNotSubscribedError"; +this.GECKO_CALL_ERROR_INCOMING_CALLS_BARRED_WITHIN_CUG = "IncomingCallsBarredWithinCugError"; +this.GECKO_CALL_ERROR_BEARER_CAPABILITY_NOT_AUTHORIZED = "BearerCapabilityNotAuthorizedError"; +this.GECKO_CALL_ERROR_BEARER_CAPABILITY_NOT_AVAILABLE = "BearerCapabilityNotAvailableError"; +this.GECKO_CALL_ERROR_BEARER_NOT_IMPLEMENTED = "BearerNotImplementedError"; +this.GECKO_CALL_ERROR_SERVICE_NOT_AVAILABLE = "ServiceNotAvailableError"; +this.GECKO_CALL_ERROR_INCOMING_CALL_EXCEEDED = "IncomingCallExceededError"; +this.GECKO_CALL_ERROR_REQUESTED_FACILITY_NOT_IMPLEMENTED = "RequestedFacilityNotImplementedError"; +this.GECKO_CALL_ERROR_UNRESTRICTED_BEARER_NOT_AVAILABLE = "UnrestrictedBearerNotAvailableError"; +this.GECKO_CALL_ERROR_SERVICE_NOT_IMPLEMENTED = "ServiceNotImplementedError"; +this.GECKO_CALL_ERROR_INVALID_TRANSACTION_ID = "InvalidTransactionIdError"; +this.GECKO_CALL_ERROR_USER_NOT_CUG_MEMBER = "NotCugMemberError"; +this.GECKO_CALL_ERROR_INCOMPATIBLE_DESTINATION = "IncompatibleDestinationError"; +this.GECKO_CALL_ERROR_INVALID_TRANSIT_NETWORK_SELECTION = "InvalidTransitNetworkSelectionError"; +this.GECKO_CALL_ERROR_SEMANTICALLY_INCORRECT_MESSAGE = "SemanticallyIncorrectMessageError"; +this.GECKO_CALL_ERROR_INVALID_MANDATORY_INFO = "InvalidMandatoryInfoError"; +this.GECKO_CALL_ERROR_MESSAGE_TYPE_NOT_IMPLEMENTED = "MessageTypeNotImplementedError"; +this.GECKO_CALL_ERROR_MESSAGE_TYPE_INCOMPATIBLE_PROTOCOL_STATE = "MessageTypeIncompatibleProtocolStateError"; +this.GECKO_CALL_ERROR_INFO_ELEMENT_NOT_IMPLEMENTED = "InfoElementNotImplementedError"; +this.GECKO_CALL_ERROR_CONDITIONAL_IE = "ConditionalIeError"; +this.GECKO_CALL_ERROR_MESSAGE_INCOMPATIBLE_PROTOCOL_STATE = "MessageIncompatibleProtocolStateError"; +this.GECKO_CALL_ERROR_RECOVERY_ON_TIMER_EXPIRY = "RecoveryOnTimerExpiryError"; +this.GECKO_CALL_ERROR_PROTOCOL = "ProtocolError"; +this.GECKO_CALL_ERROR_INTERWORKING = "InterworkingError"; +this.GECKO_CALL_ERROR_BARRED = "BarredError"; +this.GECKO_CALL_ERROR_FDN_BLOCKED = "FDNBlockedError"; +this.GECKO_CALL_ERROR_SUBSCRIBER_UNKNOWN = "SubscriberUnknownError"; +this.GECKO_CALL_ERROR_DEVICE_NOT_ACCEPTED = "DeviceNotAcceptedError"; +this.GECKO_CALL_ERROR_MODIFIED_TO_DIAL_FAILED = "ModifiedDialError"; +this.GECKO_CALL_ERROR_CDMA_LOCKED_UNTIL_POWER_CYCLE = "CdmaLockedUntilPowerCycleError"; +this.GECKO_CALL_ERROR_CDMA_DROP = "CdmaDropError"; +this.GECKO_CALL_ERROR_CDMA_INTERCEPT = "CdmaInterceptError"; +this.GECKO_CALL_ERROR_CDMA_REORDER = "CdmaReorderError"; +this.GECKO_CALL_ERROR_CDMA_SO_REJECT = "CdmaSoRejectError"; +this.GECKO_CALL_ERROR_CDMA_RETRY_ORDER = "CdmaRetryOrderError"; +this.GECKO_CALL_ERROR_CDMA_ACCESS_FAILURE = "CdmaAcessError"; +this.GECKO_CALL_ERROR_CDMA_PREEMPTED = "CdmaPreemptedError"; +this.GECKO_CALL_ERROR_CDMA_NOT_EMERGENCY = "CdmaNotEmergencyError"; +this.GECKO_CALL_ERROR_CDMA_ACCESS_BLOCKED = "CdmaAccessBlockedError"; +this.GECKO_CALL_ERROR_UNSPECIFIED = "UnspecifiedError"; + +this.RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR = {}; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_UNOBTAINABLE_NUMBER] = GECKO_CALL_ERROR_BAD_NUMBER; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_NO_ROUTE_TO_DESTINATION] = GECKO_CALL_ERROR_NO_ROUTE_TO_DESTINATION; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CHANNEL_UNACCEPTABLE] = GECKO_CALL_ERROR_CHANNEL_UNACCEPTABLE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_OPERATOR_DETERMINED_BARRING] = GECKO_CALL_ERROR_OPERATOR_DETERMINED_BARRING; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_NORMAL] = GECKO_CALL_ERROR_NORMAL_CALL_CLEARING; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_BUSY] = GECKO_CALL_ERROR_BUSY; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_NO_USER_RESPONDING] = GECKO_CALL_ERROR_NO_USER_RESPONDING; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_USER_ALERTING] = GECKO_CALL_ERROR_USER_ALERTING; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CALL_REJECTED] = GECKO_CALL_ERROR_REJECTED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_NUMBER_CHANGED] = GECKO_CALL_ERROR_NUMBER_CHANGED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CALL_REJECTED_DESTINATION_FEATURE] = GECKO_CALL_ERROR_REJECTED_DETINATION_FEATURE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CALL_PRE_EMPTION] = GECKO_CALL_ERROR_PRE_EMPTION; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_DEST_OUT_OF_ORDER] = GECKO_CALL_ERROR_DEST_OUT_OF_ORDER; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_INVALID_FORMAT] = GECKO_CALL_ERROR_INVALID_NUMBER_FORMAT; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_FACILITY_REJECTED] = GECKO_CALL_ERROR_FACILITY_REJECTED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_RESPONSE_TO_STATUS_ENQUIRY] = GECKO_CALL_ERROR_RESPONSE_TO_STATUS_ENQUIRY; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_NORMAL_UNSPECIFIED] = GECKO_CALL_ERROR_NORMAL_CALL_CLEARING; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CONGESTION] = GECKO_CALL_ERROR_CONGESTION; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_NETWORK_OUT_OF_ORDER] = GECKO_CALL_ERROR_NETWORK_OUT_OF_ORDER; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_NETWORK_TEMP_FAILURE] = GECKO_CALL_ERROR_NETWORK_TEMP_FAILURE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_SWITCHING_EQUIP_CONGESTION] = GECKO_CALL_ERROR_SWITCHING_EQUIP_CONGESTION; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_ACCESS_INFO_DISCARDED] = GECKO_CALL_ERROR_ACCESS_INFO_DISCARDED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_REQUESTED_CHANNEL_NOT_AVAILABLE] = GECKO_CALL_ERROR_REQUESTED_CHANNEL_NOT_AVAILABLE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_RESOURCE_UNAVAILABLE] = GECKO_CALL_ERROR_RESOURCE_UNAVAILABLE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_QOS_UNAVAILABLE] = GECKO_CALL_ERROR_QOS_UNAVAILABLE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_REQUESTED_FACILITY_NOT_SUBSCRIBED] = GECKO_CALL_ERROR_REQUESTED_FACILITY_NOT_SUBSCRIBED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_INCOMING_CALLS_BARRED_WITHIN_CUG] = GECKO_CALL_ERROR_INCOMING_CALLS_BARRED_WITHIN_CUG; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_BEARER_CAPABILITY_NOT_AUTHORIZED] = GECKO_CALL_ERROR_BEARER_CAPABILITY_NOT_AUTHORIZED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_BEARER_CAPABILITY_NOT_AVAILABLE] = GECKO_CALL_ERROR_BEARER_CAPABILITY_NOT_AVAILABLE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_SERVICE_NOT_AVAILABLE] = GECKO_CALL_ERROR_SERVICE_NOT_AVAILABLE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_BEARER_NOT_IMPLEMENTED] = GECKO_CALL_ERROR_BEARER_NOT_IMPLEMENTED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_ACM_LIMIT_EXCEEDED] = GECKO_CALL_ERROR_INCOMING_CALL_EXCEEDED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_REQUESTED_FACILITY_NOT_IMPLEMENTED] = GECKO_CALL_ERROR_REQUESTED_FACILITY_NOT_IMPLEMENTED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_UNRESTRICTED_BEARER_NOT_AVAILABLE] = GECKO_CALL_ERROR_UNRESTRICTED_BEARER_NOT_AVAILABLE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_SERVICE_NOT_IMPLEMENTED] = GECKO_CALL_ERROR_SERVICE_NOT_IMPLEMENTED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_INVALID_TRANSACTION_ID] = GECKO_CALL_ERROR_INVALID_TRANSACTION_ID; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_USER_NOT_CUG_MEMBER] = GECKO_CALL_ERROR_USER_NOT_CUG_MEMBER; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_INCOMPATIBLE_DESTINATION] = GECKO_CALL_ERROR_INCOMPATIBLE_DESTINATION; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_INVALID_TRANSIT_NETWORK_SELECTION] = GECKO_CALL_ERROR_INVALID_TRANSIT_NETWORK_SELECTION; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_SEMANTICALLY_INCORRECT_MESSAGE] = GECKO_CALL_ERROR_SEMANTICALLY_INCORRECT_MESSAGE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_INVALID_MANDATORY_INFO] = GECKO_CALL_ERROR_INVALID_MANDATORY_INFO; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_MESSAGE_TYPE_NOT_IMPLEMENTED] = GECKO_CALL_ERROR_MESSAGE_TYPE_NOT_IMPLEMENTED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_MESSAGE_TYPE_INCOMPATIBLE_PROTOCOL_STATE] = GECKO_CALL_ERROR_MESSAGE_TYPE_INCOMPATIBLE_PROTOCOL_STATE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_INFO_ELEMENT_NOT_IMPLEMENTED] = GECKO_CALL_ERROR_INFO_ELEMENT_NOT_IMPLEMENTED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CONDITIONAL_IE_ERROR] = GECKO_CALL_ERROR_CONDITIONAL_IE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_MESSAGE_INCOMPABITLE_PROTOCOL_STATE] = GECKO_CALL_ERROR_MESSAGE_INCOMPATIBLE_PROTOCOL_STATE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_RECOVERY_ON_TIMER_EXPIRY] = GECKO_CALL_ERROR_RECOVERY_ON_TIMER_EXPIRY; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_PROTOCOL_ERROR] = GECKO_CALL_ERROR_PROTOCOL; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_INTERWORKING] = GECKO_CALL_ERROR_INTERWORKING; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CALL_BARRED] = GECKO_CALL_ERROR_BARRED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_FDN_BLOCKED] = GECKO_CALL_ERROR_FDN_BLOCKED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_IMSI_UNKNOWN_IN_VLR] = GECKO_CALL_ERROR_SUBSCRIBER_UNKNOWN; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_IMEI_NOT_ACCEPTED] = GECKO_CALL_ERROR_DEVICE_NOT_ACCEPTED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_DIAL_MODIFIED_TO_USSD] = GECKO_CALL_ERROR_MODIFIED_TO_DIAL_FAILED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_DIAL_MODIFIED_TO_SS] = GECKO_CALL_ERROR_MODIFIED_TO_DIAL_FAILED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_DIAL_MODIFIED_TO_DIAL] = GECKO_CALL_ERROR_MODIFIED_TO_DIAL_FAILED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_LOCKED_UNTIL_POWER_CYCLE] = GECKO_CALL_ERROR_CDMA_LOCKED_UNTIL_POWER_CYCLE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_DROP] = GECKO_CALL_ERROR_CDMA_DROP; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_INTERCEPT] = GECKO_CALL_ERROR_CDMA_INTERCEPT; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_REORDER] = GECKO_CALL_ERROR_CDMA_REORDER; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_SO_REJECT] = GECKO_CALL_ERROR_CDMA_SO_REJECT; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_RETRY_ORDER] = GECKO_CALL_ERROR_CDMA_RETRY_ORDER; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_ACCESS_FAILURE] = GECKO_CALL_ERROR_CDMA_ACCESS_FAILURE; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_PREEMPTED] = GECKO_CALL_ERROR_CDMA_PREEMPTED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_NOT_EMERGENCY] = GECKO_CALL_ERROR_CDMA_NOT_EMERGENCY; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_CDMA_ACCESS_BLOCKED] = GECKO_CALL_ERROR_CDMA_ACCESS_BLOCKED; +RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_ERROR_UNSPECIFIED] = GECKO_CALL_ERROR_UNSPECIFIED; + +this.GECKO_DATACALL_ERROR_OPERATOR_BARRED = "OperatorBarredError"; +this.GECKO_DATACALL_ERROR_INSUFFICIENT_RESOURCES = "InsufficientResourcesError"; +this.GECKO_DATACALL_ERROR_MISSING_UKNOWN_APN = "MissingUnknownAPNError"; +this.GECKO_DATACALL_ERROR_UNKNOWN_PDP_ADDRESS_TYPE = "UnknownPDPAddressTypeError"; +this.GECKO_DATACALL_ERROR_USER_AUTHENTICATION = "UserAuthenticationError"; +this.GECKO_DATACALL_ERROR_ACTIVATION_REJECT_GGSN = "ActivationRejectGGSNError"; +this.GECKO_DATACALL_ERROR_ACTIVATION_REJECT_UNSPECIFIED = "ActivationRejectUnspecifiedError"; +this.GECKO_DATACALL_ERROR_SERVICE_OPTION_NOT_SUPPORTED = "ServiceOptionNotSupportedError"; +this.GECKO_DATACALL_ERROR_SERVICE_OPTION_NOT_SUBSCRIBED = "ServiceOptionNotSubscribedError"; +this.GECKO_DATACALL_ERROR_SERVICE_OPTION_OUT_OF_ORDER = "ServiceOptionOutOfOrderError"; +this.GECKO_DATACALL_ERROR_NSAPI_IN_USE = "NSAPIInUseError"; +this.GECKO_DATACALL_ERROR_ONLY_IPV4_ALLOWED = "OnlyIPv4Error"; +this.GECKO_DATACALL_ERROR_ONLY_IPV6_ALLOWED = "OnlyIPv6Error"; +this.GECKO_DATACALL_ERROR_ONLY_SINGLE_BEARER_ALLOWED = "OnlySingleBearerAllowedError"; +this.GECKO_DATACALL_ERROR_PROTOCOL_ERRORS = "ProtocolErrorsError"; +this.GECKO_DATACALL_ERROR_VOICE_REGISTRATION_FAIL = "VoiceRegistrationFailError"; +this.GECKO_DATACALL_ERROR_DATA_REGISTRATION_FAIL = "DataRegistrationFailError"; +this.GECKO_DATACALL_ERROR_SIGNAL_LOST = "SignalLostError"; +this.GECKO_DATACALL_ERROR_PREF_RADIO_TECH_CHANGED = "PrefRadioTechChangedError"; +this.GECKO_DATACALL_ERROR_RADIO_POWER_OFF = "RadioPowerOffError"; +this.GECKO_DATACALL_ERROR_TETHERED_CALL_ACTIVE = "TetheredCallActiveError"; +this.GECKO_DATACALL_ERROR_UNSPECIFIED = "UnspecifiedError"; + +this.RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR = {}; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_OPERATOR_BARRED] = GECKO_DATACALL_ERROR_OPERATOR_BARRED; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_INSUFFICIENT_RESOURCES] = GECKO_DATACALL_ERROR_INSUFFICIENT_RESOURCES; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_MISSING_UKNOWN_APN] = GECKO_DATACALL_ERROR_MISSING_UKNOWN_APN; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_UNKNOWN_PDP_ADDRESS_TYPE] = GECKO_DATACALL_ERROR_UNKNOWN_PDP_ADDRESS_TYPE; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_USER_AUTHENTICATION] = GECKO_DATACALL_ERROR_USER_AUTHENTICATION; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_ACTIVATION_REJECT_GGSN] = GECKO_DATACALL_ERROR_ACTIVATION_REJECT_GGSN; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_ACTIVATION_REJECT_UNSPECIFIED] = GECKO_DATACALL_ERROR_ACTIVATION_REJECT_UNSPECIFIED; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_SERVICE_OPTION_NOT_SUPPORTED] = GECKO_DATACALL_ERROR_SERVICE_OPTION_NOT_SUPPORTED; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_SERVICE_OPTION_NOT_SUBSCRIBED] = GECKO_DATACALL_ERROR_SERVICE_OPTION_NOT_SUBSCRIBED; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_SERVICE_OPTION_OUT_OF_ORDER] = GECKO_DATACALL_ERROR_SERVICE_OPTION_OUT_OF_ORDER; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_NSAPI_IN_USE] = GECKO_DATACALL_ERROR_NSAPI_IN_USE; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_ONLY_IPV4_ALLOWED] = GECKO_DATACALL_ERROR_ONLY_IPV4_ALLOWED; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_ONLY_IPV6_ALLOWED] = GECKO_DATACALL_ERROR_ONLY_IPV6_ALLOWED; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_ONLY_SINGLE_BEARER_ALLOWED] = GECKO_DATACALL_ERROR_ONLY_SINGLE_BEARER_ALLOWED; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_PROTOCOL_ERRORS] = GECKO_DATACALL_ERROR_PROTOCOL_ERRORS; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_VOICE_REGISTRATION_FAIL] = GECKO_DATACALL_ERROR_VOICE_REGISTRATION_FAIL; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_DATA_REGISTRATION_FAIL] = GECKO_DATACALL_ERROR_DATA_REGISTRATION_FAIL; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_SIGNAL_LOST] = GECKO_DATACALL_ERROR_SIGNAL_LOST; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_PREF_RADIO_TECH_CHANGED] = GECKO_DATACALL_ERROR_PREF_RADIO_TECH_CHANGED; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_RADIO_POWER_OFF] = GECKO_DATACALL_ERROR_RADIO_POWER_OFF; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_TETHERED_CALL_ACTIVE] = GECKO_DATACALL_ERROR_TETHERED_CALL_ACTIVE; +RIL_DATACALL_FAILCAUSE_TO_GECKO_DATACALL_ERROR[DATACALL_FAIL_ERROR_UNSPECIFIED] = GECKO_DATACALL_ERROR_UNSPECIFIED; + +this.GECKO_RADIO_TECH = [ + null, + "gprs", + "edge", + "umts", + "is95a", + "is95b", + "1xrtt", + "evdo0", + "evdoa", + "hsdpa", + "hsupa", + "hspa", + "evdob", + "ehrpd", + "lte", + "hspa+", + "gsm", + null, + "hspa+", // DC-HSPA+ + "hspa+" +]; + +this.GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN = -1; + +// Call forwarding action. Must be in sync with nsIMobileConnectionService interface +this.CALL_FORWARD_ACTION_DISABLE = 0; +this.CALL_FORWARD_ACTION_ENABLE = 1; +this.CALL_FORWARD_ACTION_QUERY_STATUS = 2; +this.CALL_FORWARD_ACTION_REGISTRATION = 3; +this.CALL_FORWARD_ACTION_ERASURE = 4; + +// Call forwarding reason. Must be in sync with nsIMobileConnectionService interface +this.CALL_FORWARD_REASON_UNCONDITIONAL = 0; +this.CALL_FORWARD_REASON_MOBILE_BUSY = 1; +this.CALL_FORWARD_REASON_NO_REPLY = 2; +this.CALL_FORWARD_REASON_NOT_REACHABLE = 3; +this.CALL_FORWARD_REASON_ALL_CALL_FORWARDING = 4; +this.CALL_FORWARD_REASON_ALL_CONDITIONAL_CALL_FORWARDING = 5; + +// Call barring program. Must be in sync with nsIMobileConnectionService interface +this.CALL_BARRING_PROGRAM_ALL_OUTGOING = 0; +this.CALL_BARRING_PROGRAM_OUTGOING_INTERNATIONAL = 1; +this.CALL_BARRING_PROGRAM_OUTGOING_INTERNATIONAL_EXCEPT_HOME = 2; +this.CALL_BARRING_PROGRAM_ALL_INCOMING = 3; +this.CALL_BARRING_PROGRAM_INCOMING_ROAMING = 4; +this.CALL_BARRING_PROGRAM_ALL_SERVICE = 5; +this.CALL_BARRING_PROGRAM_OUTGOING_SERVICE = 6; +this.CALL_BARRING_PROGRAM_INCOMING_SERVICE = 7; + +this.CALL_BARRING_PROGRAM_TO_FACILITY = {}; +CALL_BARRING_PROGRAM_TO_FACILITY[CALL_BARRING_PROGRAM_ALL_OUTGOING] = ICC_CB_FACILITY_BAOC; +CALL_BARRING_PROGRAM_TO_FACILITY[CALL_BARRING_PROGRAM_OUTGOING_INTERNATIONAL] = ICC_CB_FACILITY_BAOIC; +CALL_BARRING_PROGRAM_TO_FACILITY[CALL_BARRING_PROGRAM_OUTGOING_INTERNATIONAL_EXCEPT_HOME] = ICC_CB_FACILITY_BAOICxH; +CALL_BARRING_PROGRAM_TO_FACILITY[CALL_BARRING_PROGRAM_ALL_INCOMING] = ICC_CB_FACILITY_BAIC; +CALL_BARRING_PROGRAM_TO_FACILITY[CALL_BARRING_PROGRAM_INCOMING_ROAMING] = ICC_CB_FACILITY_BAICr; +CALL_BARRING_PROGRAM_TO_FACILITY[CALL_BARRING_PROGRAM_ALL_SERVICE] = ICC_CB_FACILITY_BA_ALL; +CALL_BARRING_PROGRAM_TO_FACILITY[CALL_BARRING_PROGRAM_OUTGOING_SERVICE] = ICC_CB_FACILITY_BA_MO; +CALL_BARRING_PROGRAM_TO_FACILITY[CALL_BARRING_PROGRAM_INCOMING_SERVICE] = ICC_CB_FACILITY_BA_MT; + +/** + * CDMA PDU constants + */ + +// SMS Message Type, as defined in 3GPP2 C.S0015-A v2.0, Table 3.4-1 +this.PDU_CDMA_MSG_TYPE_P2P = 0x00; // Point-to-Point +this.PDU_CDMA_MSG_TYPE_BROADCAST = 0x01; // Broadcast +this.PDU_CDMA_MSG_TYPE_ACK = 0x02; // Acknowledge + +// SMS Teleservice Identitifier, as defined in 3GPP2 N.S0005, Table 175 +this.PDU_CDMA_MSG_TELESERIVCIE_ID_SMS = 0x1002; // SMS +this.PDU_CDMA_MSG_TELESERIVCIE_ID_WAP = 0x1004; // WAP +this.PDU_CDMA_MSG_TELESERIVCIE_ID_WEMT = 0x1005; // Wireless Enhanced Messaging Teleservice + // required for fragmented SMS + +// SMS Service Category, as defined in 3GPP2 C.R1001-D, Table 9.3.1-1 +this.PDU_CDMA_MSG_CATEGORY_UNSPEC = 0x00; // Unknown/Unspecified + +// Address Information, Digit Mode, as defined in 3GPP2 C.S0015-A v2.0, sec 3.4.3.3 +this.PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF = 0x00; // Digit Mode : DTMF +this.PDU_CDMA_MSG_ADDR_DIGIT_MODE_ASCII = 0x01; // Digit Mode : 8-bit ASCII with MSB = 0 + +// Address Information, Number Mode, as defined in 3GPP2 C.S0015-A v2.0, sec 3.4.3.3 +this.PDU_CDMA_MSG_ADDR_NUMBER_MODE_ANSI = 0x00; // Number Mode : ANSI T1.607-2000(R2004) +this.PDU_CDMA_MSG_ADDR_NUMBER_MODE_ASCII = 0x01; // Number Mode : Data network address format + +// Address Information, Number Type, as defined in 3GPP2 C.S0015-A v2.0, Table 3.4.3.3-1 +this.PDU_CDMA_MSG_ADDR_NUMBER_TYPE_UNKNOWN = 0x00; // Number Type : Unknown +this.PDU_CDMA_MSG_ADDR_NUMBER_TYPE_INTERNATIONAL = 0x01; // Number Type : Internaltional number(+XXXXX) +this.PDU_CDMA_MSG_ADDR_NUMBER_TYPE_NATIONAL = 0x02; // Number Type : National number + +// Address Information, Number Plan, as defined in 3GPP2 C.S0005-D v2.0, Table 2.7.1.3.2.4-3 +this.PDU_CDMA_MSG_ADDR_NUMBER_PLAN_UNKNOWN = 0x00; // Number Plan : Unknown +this.PDU_CDMA_MSG_ADDR_NUMBER_PLAN_ISDN = 0x01; // Number Plan : ISDN/Telephony numbering plan + +// SMS Encoding, as defined in 3GPP2 C.R1001-D, Table 9.1-1 +this.PDU_CDMA_MSG_CODING_OCTET = 0x00; // octet(8-bit), Not tested +this.PDU_CDMA_MSG_CODING_IS_91 = 0x01; // IS-91 Extended Protocol Message(variable), Not tested +this.PDU_CDMA_MSG_CODING_7BITS_ASCII = 0x02; // 7-bit ASCII(7-bit) +this.PDU_CDMA_MSG_CODING_IA5 = 0x03; // IA5(7-bit), Not tested +this.PDU_CDMA_MSG_CODING_UNICODE = 0x04; // Unicode(16-bit) +this.PDU_CDMA_MSG_CODING_SHIFT_JIS = 0x05; // Shift-6 JIS(8/16-bit variable), Not supported +this.PDU_CDMA_MSG_CODING_KOREAN = 0x06; // Korean(8/16-bit variable), Not supported +this.PDU_CDMA_MSG_CODING_LATIN_HEBREW = 0x07; // Latin/ Hebrew(8-bit), ISO/IEC 8859-8, Not supported +this.PDU_CDMA_MSG_CODING_LATIN = 0x08; // Latin(8-bit), ISO/IEC 8859-1, Not tested +this.PDU_CDMA_MSG_CODING_7BITS_GSM = 0x09; // GSM 7-bit default alphabet(7-bit), Not tested +this.PDU_CDMA_MSG_CODING_GSM_DCS = 0x0A; // GSM Data-Coding-Scheme, Not supported + +// SMS Message Type, as defined in 3GPP2 C.S0015-A v2.0, Table 4.5.1-1 +this.PDU_CDMA_MSG_TYPE_DELIVER = 0x01; // Deliver +this.PDU_CDMA_MSG_TYPE_SUBMIT = 0x02; // Submit +this.PDU_CDMA_MSG_TYPE_DELIVER_ACK = 0x04; // Delivery Acknowledgment + +// SMS User Data Subparameters, as defined in 3GPP2 C.S0015-A v2.0, Table 4.5-1 +this.PDU_CDMA_MSG_USERDATA_MSG_ID = 0x00; // Message Identifier +this.PDU_CDMA_MSG_USERDATA_BODY = 0x01; // User Data Body +this.PDU_CDMA_MSG_USERDATA_TIMESTAMP = 0x03; // Message Center Time Stamp +this.PDU_CDMA_MSG_USERDATA_REPLY_OPTION = 0x0A; // Reply Option +this.PDU_CDMA_LANGUAGE_INDICATOR = 0x0D; // Language Indicator +this.PDU_CDMA_MSG_USERDATA_CALLBACK_NUMBER = 0x0E; // Callback Number +this.PDU_CDMA_MSG_USER_DATA_MSG_STATUS = 0x14; // Message Status + +// CDMA Language Indicator: Language groups +// see 3GPP2 C.R1001-F table 9.2-1 +this.CB_CDMA_LANG_GROUP = [ + null, "en", "fr", "es", "ja", "ko", "zh", "he" +]; + +// IS-91 Message Type, as defined in TIA/EIA/IS-91-A, Table 9 +this.PDU_CDMA_MSG_CODING_IS_91_TYPE_VOICEMAIL_STATUS = 0x82; +this.PDU_CDMA_MSG_CODING_IS_91_TYPE_SMS_FULL = 0x83; +this.PDU_CDMA_MSG_CODING_IS_91_TYPE_CLI = 0x84; +this.PDU_CDMA_MSG_CODING_IS_91_TYPE_SMS = 0x85; + +// Information Record Type, reference from ril.h +this.PDU_CDMA_INFO_REC_TYPE_DISPLAY = 0; +this.PDU_CDMA_INFO_REC_TYPE_CALLED_PARTY_NUMBER = 1; +this.PDU_CDMA_INFO_REC_TYPE_CALLING_PARTY_NUMBER = 2; +this.PDU_CDMA_INFO_REC_TYPE_CONNECTED_NUMBER =3; +this.PDU_CDMA_INFO_REC_TYPE_SIGNAL = 4; +this.PDU_CDMA_INFO_REC_TYPE_REDIRECTING_NUMBER = 5; +this.PDU_CDMA_INFO_REC_TYPE_LINE_CONTROL = 6; +this.PDU_CDMA_INFO_REC_TYPE_EXTENDED_DISPLAY = 7; +this.PDU_CDMA_INFO_REC_TYPE_T53_CLIR = 8; +this.PDU_CDMA_INFO_REC_TYPE_T53_RELEASE = 9; +this.PDU_CDMA_INFO_REC_TYPE_T53_AUDIO_CONTROL = 10; + +// Display type of extended display of information record, +// as defined in C.S0005-F v1.0, Table 3.7.5.16-2 +this.INFO_REC_EXTENDED_DISPLAY_BLANK = 0x80; +this.INFO_REC_EXTENDED_DISPLAY_SKIP = 0x81; +this.INFO_REC_EXTENDED_DISPLAY_CONTINATION = 0x82; +this.INFO_REC_EXTENDED_DISPLAY_CALLED_ADDRESS = 0x83; +this.INFO_REC_EXTENDED_DISPLAY_CAUSE = 0x84; +this.INFO_REC_EXTENDED_DISPLAY_PROGRESS_INDICATOR = 0x85; +this.INFO_REC_EXTENDED_DISPLAY_NOTIFICATION_INDICATOR = 0x86; +this.INFO_REC_EXTENDED_DISPLAY_PROMPT = 0x87; +this.INFO_REC_EXTENDED_DISPLAY_ACCUMULATED_DIGITS = 0x88; +this.INFO_REC_EXTENDED_DISPLAY_STATUS = 0x89; +this.INFO_REC_EXTENDED_DISPLAY_INBAND = 0x8A; +this.INFO_REC_EXTENDED_DISPLAY_CALLING_ADDRESS = 0x8B; +this.INFO_REC_EXTENDED_DISPLAY_REASON = 0x8C; +this.INFO_REC_EXTENDED_DISPLAY_CALLING_PARTY_NAME = 0x8D; +this.INFO_REC_EXTENDED_DISPLAY_CALLED_PARTY_NAME = 0x8E; +this.INFO_REC_EXTENDED_DISPLAY_ORIGINAL_CALLED_NAME = 0x8F; +this.INFO_REC_EXTENDED_DISPLAY_REDIRECT_NAME = 0x90; +this.INFO_REC_EXTENDED_DISPLAY_CONNECTED_NAME = 0x91; +this.INFO_REC_EXTENDED_DISPLAY_ORIGINATING_RESTRICTIONS = 0x92; +this.INFO_REC_EXTENDED_DISPLAY_DATE_TIME_OF_DAY = 0x93; +this.INFO_REC_EXTENDED_DISPLAY_CALL_APPEARANCE_ID = 0x94; +this.INFO_REC_EXTENDED_DISPLAY_FEATURE_ADDRESS = 0x95; +this.INFO_REC_EXTENDED_DISPLAY_REDIRECTION_NAME = 0x96; +this.INFO_REC_EXTENDED_DISPLAY_REDIRECTION_NUMBER = 0x97; +this.INFO_REC_EXTENDED_DISPLAY_REDIRECTING_NUMBER = 0x98; +this.INFO_REC_EXTENDED_DISPLAY_ORIGINAL_CALLED_NUMBER = 0x99; +this.INFO_REC_EXTENDED_DISPLAY_CONNECTED_NUMBER = 0x9A; +this.INFO_REC_EXTENDED_DISPLAY_TEXT = 0x9B; + +/** + * The table for MCC/MNC which the length of MNC is 3. + * + * This table is built from below links. + * - http://www.itu.int/pub/T-SP-E.212B-2013 + * - http://en.wikipedia.org/wiki/Mobile_Network_Code + */ +this.PLMN_HAVING_3DIGITS_MNC = { + // Puerto Rico. + "330": + ["110", // América Móvil + "120" // PR Wireless + ], + // Trinidad and Tobago. + "374": + ["130", // Digicel Trinidad and Tobago Ltd. + "140" // LaqTel Ltd. + ], + // India. + "405": + ["000", // Shyam Telelink Ltd. + "005", // Reliance, Delhi + "006", // Reliance, Gujarat + "007", // Reliance, Haryana + "009", // Reliance, J&K + "010", // Reliance, Karnataka + "011", // Reliance, Kerala + "012", // Reliance, Andhra Pradesh + "013", // Reliance, Maharashtr + "014", // Reliance, Madhya Pradesh + "018", // Reliance, Punjab + "020", // Reliance, Tamilnadu + "021", // Reliance, UP (East) + "022", // Reliance, UP (West) + "025", // TATA DOCOMO, Andhra Pradesh + "026", // TATA DOCOMO, Assam + "027", // TATA DOCOMO, Bihar + "028", // TATA DOCOMO, Chennai + "029", // TATA DOCOMO, Delhi + "030", // TATA DOCOMO, Gujarat + "031", // TATA DOCOMO, Haryana + "032", // TATA DOCOMO, Himachal Pradesh + "033", // Reliance, Bihar + "034", // TATA DOCOMO, Kamataka + "035", // TATA DOCOMO, Kerala + "036", // TATA DOCOMO, Kolkata + "037", // TATA DOCOMO, Maharashtra + "038", // TATA DOCOMO, Madhya Pradesh + "039", // TATA DOCOMO, Mumbai + "040", // Reliance, Chennai + "041", // TATA DOCOMO, Orissa + "042", // TATA DOCOMO, Punjab + "043", // TATA DOCOMO, Rajasthan + "044", // TATA DOCOMO, Tamilnadu + "045", // TATA DOCOMO, UP (East) + "046", // TATA DOCOMO, UP (West) + "047", // TATA DOCOMO, West Bengal + "750", // Vodafone IN, J&K + "751", // Vodafone IN, Assam + "752", // Vodafone IN, Bihar + "753", // Vodafone IN, Orissa + "754", // Vodafone IN, Himachal Pradesh + "755", // Vodafone IN, North East + "756", // Vodafone IN, Madhya Pradesh & Chhattisgarh + "799", // Idea, MUMBAI + "800", // Aircell, Delhi + "801", // Aircell, Andhra Pradesh + "802", // Aircell, Gujarat + "803", // Aircell, Kamataka + "804", // Aircell, Maharashtra + "805", // Aircell, Mumbai + "806", // Aircell, Rajasthan + "807", // Aircell, Haryana + "808", // Aircell, Madhya Pradesh + "809", // Aircell, Kerala + "810", // Aircell, Uttar Pradesh (East) + "811", // Aircell, Uttar Pradesh (West) + "812", // Aircell, Punjab + "818", // Uninor, Uttar Pradesh (West) + "819", // Uninor, Andhra Pradesh + "820", // Uninor, Karnataka + "821", // Uninor, Kerala + "822", // Uninor, Kolkata + "824", // Videocon, Assam + "827", // Videocon, Gujarat + "834", // Videocon, Madhya Pradesh + "840", // Jio, West Bengal + "844", // Uninor, Delhi & NCR + "845", // IDEA, Assam + "846", // IDEA, Jammu & Kashmir + "847", // IDEA, Karnataka + "848", // IDEA, Kolkata + "849", // IDEA, North East + "850", // IDEA, Orissa + "851", // IDEA, Punjab + "852", // IDEA, Tamil Nadu + "853", // IDEA, West Bengal + "854", // Jio, Andra Pradesh + "855", // Jio, Assam + "856", // Jio, Bihar + "857", // Jio, Gujarat + "858", // Jio, Haryana + "859", // Jio, Himachal Pradesh + "860", // Jio, Jammu Kashmir + "861", // Jio, Karnataka + "862", // Jio, Kerala + "863", // Jio, Madhyya Pradesh + "864", // Jio, Maharashtra + "865", // Jio, North East + "866", // Jio, Orissa + "867", // Jio, Punjab + "868", // Jio, Rajasthan + "869", // Jio, Tamil Nadu Chennai + "870", // Jio, Uttar Pradesh West + "871", // Jio, Uttar Pradesh East + "872", // Jio, Delhi + "873", // Jio, Kolkatta + "874", // Jio, Mumbai + "875", // Uninor, Assam + "880", // Uninor, West Bengal + "881", // S Tel, Assam + "908", // IDEA, Andhra Pradesh + "909", // IDEA, Delhi + "910", // IDEA, Haryana + "911", // Etisalat, Maharashtra + "912", // Etisalat, Andhra Pradesh + "913", // Etisalat, Delhi & NCR + "914", // Etisalat, Gujarat + "917", // Etisalat, Kerala + "927", // Uninor, Gujarat + "929" // Uninor, Maharashtra + ], + // Malaysia. + "502": + ["150", // Tune Talk Sdn Bhd + "151", // Baraka Telecom Sdn Bhd (MVNE) + "152", // YTL Communications Sdn Bhd + "156" // Altel Communications Sdn Bhd + ], + // Brazil. + "724": + ["055" // Sercomtel + ] +}; + +/** + * The table for MCC which the length of MNC is 3 + * + * This table is built from below links. + * - http://www.itu.int/pub/T-SP-E.212B-2013 + * - http://en.wikipedia.org/wiki/Mobile_Network_Code + */ +this.MCC_TABLE_FOR_MNC_LENGTH_IS_3 = [ + "302", // Canada + "310", // United States of America + "311", // United States of America + "312", // United States of America + "313", // United States of America + "316", // United States of America + "330", // Puerto Rico + "334", // Mexico + "338", // Jamaica + "342", // Barbados + "344", // Antigua and Barbuda + "346", // Cayman Islands + "348", // British Virgin Islands + "350", // Bermuda + "352", // Grenada + "354", // Montserrat + "356", // Saint Kitts and Nevis + "358", // Saint Lucia + "360", // Saint Vincent and the Grenadines + "365", // Anguilla + "366", // Dominica + "376", // Turks and Caicos Islands + "708", // Honduras + "722", // Argentina + "732", // Colombia + "750" // Falkland Islands (Malvinas) +]; + +// Supplementary service notifications, code2, as defined in 3GPP 27.007 7.17 +this.SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD = 2; +this.SUPP_SVC_NOTIFICATION_CODE2_RETRIEVED = 3; + +this.GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD = "RemoteHeld"; +this.GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED = "RemoteResumed"; + +this.GECKO_SUPP_SVC_NOTIFICATION_FROM_CODE2 = {}; +GECKO_SUPP_SVC_NOTIFICATION_FROM_CODE2[SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD] = GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD; +GECKO_SUPP_SVC_NOTIFICATION_FROM_CODE2[SUPP_SVC_NOTIFICATION_CODE2_RETRIEVED] = GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED; + +/** + * The status for an Over-the-Air Service Provisioning / Over-the-Air + * Parameter Administration (OTASP/OTAPA) session. + * + * @see 3GPP2 C.S0016 + */ +this.GECKO_OTA_STATUS_SPL_UNLOCKED = "spl_unlocked"; +this.GECKO_OTA_STATUS_SPC_RETRIES_EXCEEDED = "spc_retries_exceeded"; +this.GECKO_OTA_STATUS_A_KEY_EXCHANGED = "a_key_exchanged"; +this.GECKO_OTA_STATUS_SSD_UPDATED = "ssd_updated"; +this.GECKO_OTA_STATUS_NAM_DOWNLOADED = "nam_downloaded"; +this.GECKO_OTA_STATUS_MDN_DOWNLOADED = "mdn_downloaded"; +this.GECKO_OTA_STATUS_IMSI_DOWNLOADED = "imsi_downloaded"; +this.GECKO_OTA_STATUS_PRL_DOWNLOADED = "prl_downloaded"; +this.GECKO_OTA_STATUS_COMMITTED = "committed"; +this.GECKO_OTA_STATUS_OTAPA_STARTED = "otapa_started"; +this.GECKO_OTA_STATUS_OTAPA_STOPPED = "otapa_stopped"; +this.GECKO_OTA_STATUS_OTAPA_ABORTED = "otapa_aborted"; +this.CDMA_OTA_PROVISION_STATUS_TO_GECKO = [ + GECKO_OTA_STATUS_SPL_UNLOCKED, + GECKO_OTA_STATUS_SPC_RETRIES_EXCEEDED, + GECKO_OTA_STATUS_A_KEY_EXCHANGED, + GECKO_OTA_STATUS_SSD_UPDATED, + GECKO_OTA_STATUS_NAM_DOWNLOADED, + GECKO_OTA_STATUS_MDN_DOWNLOADED, + GECKO_OTA_STATUS_IMSI_DOWNLOADED, + GECKO_OTA_STATUS_PRL_DOWNLOADED, + GECKO_OTA_STATUS_COMMITTED, + GECKO_OTA_STATUS_OTAPA_STARTED, + GECKO_OTA_STATUS_OTAPA_STOPPED, + GECKO_OTA_STATUS_OTAPA_ABORTED +]; + +// Allow this file to be imported via Components.utils.import(). +this.EXPORTED_SYMBOLS = Object.keys(this); diff --git a/dom/system/gonk/ril_worker.js b/dom/system/gonk/ril_worker.js new file mode 100644 index 000000000..0608a5be3 --- /dev/null +++ b/dom/system/gonk/ril_worker.js @@ -0,0 +1,15206 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +/** + * This file implements the RIL worker thread. It communicates with + * the main thread to provide a high-level API to the phone's RIL + * stack, and with the RIL IPC thread to communicate with the RIL + * device itself. These communication channels use message events as + * known from Web Workers: + * + * - postMessage()/"message" events for main thread communication + * + * - postRILMessage()/"RILMessageEvent" events for RIL IPC thread + * communication. + * + * The two main objects in this file represent individual parts of this + * communication chain: + * + * - RILMessageEvent -> Buf -> RIL -> postMessage() -> nsIRadioInterfaceLayer + * - nsIRadioInterfaceLayer -> postMessage() -> RIL -> Buf -> postRILMessage() + * + * Note: The code below is purposely lean on abstractions to be as lean in + * terms of object allocations. As a result, it may look more like C than + * JavaScript, and that's intended. + */ + +/* global BufObject */ +/* global TelephonyRequestQueue */ + +"use strict"; + +importScripts("ril_consts.js"); +importScripts("resource://gre/modules/workers/require.js"); +importScripts("ril_worker_buf_object.js"); +importScripts("ril_worker_telephony_request_queue.js"); + +// set to true in ril_consts.js to see debug messages +var DEBUG = DEBUG_WORKER; +var GLOBAL = this; + +if (!this.debug) { + // Debugging stub that goes nowhere. + this.debug = function debug(message) { + dump("RIL Worker: " + message + "\n"); + }; +} + +// Timeout value for emergency callback mode. +const EMERGENCY_CB_MODE_TIMEOUT_MS = 300000; // 5 mins = 300000 ms. + +const ICC_MAX_LINEAR_FIXED_RECORDS = 0xfe; + +const GET_CURRENT_CALLS_RETRY_MAX = 3; + +var RILQUIRKS_CALLSTATE_EXTRA_UINT32; +var RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL; +var RILQUIRKS_SIM_APP_STATE_EXTRA_FIELDS; +var RILQUIRKS_SIGNAL_EXTRA_INT32; +var RILQUIRKS_AVAILABLE_NETWORKS_EXTRA_STRING; +// Needed for call-waiting on Peak device +var RILQUIRKS_EXTRA_UINT32_2ND_CALL; +// On the emulator we support querying the number of lock retries +var RILQUIRKS_HAVE_QUERY_ICC_LOCK_RETRY_COUNT; +// Ril quirk to Send STK Profile Download +var RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD; +// Ril quirk to attach data registration on demand. +var RILQUIRKS_DATA_REGISTRATION_ON_DEMAND; +// Ril quirk to control the uicc/data subscription. +var RILQUIRKS_SUBSCRIPTION_CONTROL; +// Ril quirk to describe the SMSC address format. +var RILQUIRKS_SMSC_ADDRESS_FORMAT; + +/** + * The RIL state machine. + * + * This object communicates with rild via parcels and with the main thread + * via post messages. It maintains state about the radio, ICC, calls, etc. + * and acts upon state changes accordingly. + */ +function RilObject(aContext) { + this.context = aContext; + + this.telephonyRequestQueue = new TelephonyRequestQueue(this); + this.currentConferenceState = CALL_STATE_UNKNOWN; + this._pendingSentSmsMap = {}; + this.pendingNetworkType = {}; + this._receivedSmsCbPagesMap = {}; + this._getCurrentCallsRetryCount = 0; +} +RilObject.prototype = { + context: null, + + /** + * RIL version. + */ + version: null, + + /** + * Call state of current conference group. + */ + currentConferenceState: null, + + /** + * Outgoing messages waiting for SMS-STATUS-REPORT. + */ + _pendingSentSmsMap: null, + + /** + * Marker object. + */ + pendingNetworkType: null, + + /** + * Global Cell Broadcast switch. + */ + cellBroadcastDisabled: false, + + /** + * Parsed Cell Broadcast search lists. + * cellBroadcastConfigs.MMI should be preserved over rild reset. + */ + cellBroadcastConfigs: null, + mergedCellBroadcastConfig: null, + + _receivedSmsCbPagesMap: null, + + initRILState: function() { + /** + * One of the RADIO_STATE_* constants. + */ + this.radioState = GECKO_RADIOSTATE_UNKNOWN; + + /** + * True if we are on a CDMA phone. + */ + this._isCdma = false; + + /** + * True if we are in emergency callback mode. + */ + this._isInEmergencyCbMode = false; + + /** + * Set when radio is ready but radio tech is unknown. That is, we are + * waiting for REQUEST_VOICE_RADIO_TECH + */ + this._waitingRadioTech = false; + + /** + * Card state + */ + this.cardState = GECKO_CARDSTATE_UNINITIALIZED; + + /** + * Device Identities including IMEI, IMEISV, ESN and MEID. + */ + this.deviceIdentities = null; + + /** + * ICC information that is not exposed to Gaia. + */ + this.iccInfoPrivate = {}; + + /** + * ICC information, such as MSISDN, MCC, MNC, SPN...etc. + */ + this.iccInfo = {}; + + /** + * CDMA specific information. ex. CDMA Network ID, CDMA System ID... etc. + */ + this.cdmaHome = null; + + /** + * Application identification for apps in ICC. + */ + this.aid = null; + + /** + * Application type for apps in ICC. + */ + this.appType = null; + + this.networkSelectionMode = GECKO_NETWORK_SELECTION_UNKNOWN; + + this.voiceRegistrationState = {}; + this.dataRegistrationState = {}; + + /** + * List of strings identifying the network operator. + */ + this.operator = null; + + /** + * String containing the baseband version. + */ + this.basebandVersion = null; + + // Clean up currentCalls: rild might have restarted. + this.sendChromeMessage({ + rilMessageType: "currentCalls", + calls: {} + }); + + // Don't clean up this._pendingSentSmsMap + // because on rild restart: we may continue with the pending segments. + + /** + * Whether or not the multiple requests in requestNetworkInfo() are currently + * being processed + */ + this._processingNetworkInfo = false; + + /** + * Multiple requestNetworkInfo() in a row before finishing the first + * request, hence we need to fire requestNetworkInfo() again after + * gathering all necessary stuffs. This is to make sure that ril_worker + * gets precise network information. + */ + this._needRepollNetworkInfo = false; + + /** + * Pending messages to be send in batch from requestNetworkInfo() + */ + this._pendingNetworkInfo = {rilMessageType: "networkinfochanged"}; + + /** + * Cell Broadcast Search Lists. + */ + let cbmmi = this.cellBroadcastConfigs && this.cellBroadcastConfigs.MMI; + this.cellBroadcastConfigs = { + MMI: cbmmi || null + }; + this.mergedCellBroadcastConfig = null; + + /** + * True when the request to report SMS Memory Status is pending. + */ + this.pendingToReportSmsMemoryStatus = false; + this.smsStorageAvailable = true; + }, + + /** + * Parse an integer from a string, falling back to a default value + * if the the provided value is not a string or does not contain a valid + * number. + * + * @param string + * String to be parsed. + * @param defaultValue [optional] + * Default value to be used. + * @param radix [optional] + * A number that represents the numeral system to be used. Default 10. + */ + parseInt: function(string, defaultValue, radix) { + let number = parseInt(string, radix || 10); + if (!isNaN(number)) { + return number; + } + if (defaultValue === undefined) { + defaultValue = null; + } + return defaultValue; + }, + + + /** + * Outgoing requests to the RIL. These can be triggered from the + * main thread via messages that look like this: + * + * {rilMessageType: "methodName", + * extra: "parameters", + * go: "here"} + * + * So if one of the following methods takes arguments, it takes only one, + * an object, which then contains all of the parameters as attributes. + * The "@param" documentation is to be interpreted accordingly. + */ + + /** + * Retrieve the ICC's status. + */ + getICCStatus: function() { + this.context.Buf.simpleRequest(REQUEST_GET_SIM_STATUS); + }, + + /** + * Helper function for unlocking ICC locks. + */ + iccUnlockCardLock: function(options) { + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: + this.enterICCPIN(options); + break; + case GECKO_CARDLOCK_PIN2: + this.enterICCPIN2(options); + break; + case GECKO_CARDLOCK_PUK: + this.enterICCPUK(options); + break; + case GECKO_CARDLOCK_PUK2: + this.enterICCPUK2(options); + break; + case GECKO_CARDLOCK_NCK: + case GECKO_CARDLOCK_NSCK: + case GECKO_CARDLOCK_NCK1: + case GECKO_CARDLOCK_NCK2: + case GECKO_CARDLOCK_HNCK: + case GECKO_CARDLOCK_CCK: + case GECKO_CARDLOCK_SPCK: + case GECKO_CARDLOCK_PCK: + case GECKO_CARDLOCK_RCCK: + case GECKO_CARDLOCK_RSPCK: + case GECKO_CARDLOCK_NCK_PUK: + case GECKO_CARDLOCK_NSCK_PUK: + case GECKO_CARDLOCK_NCK1_PUK: + case GECKO_CARDLOCK_NCK2_PUK: + case GECKO_CARDLOCK_HNCK_PUK: + case GECKO_CARDLOCK_CCK_PUK: + case GECKO_CARDLOCK_SPCK_PUK: + case GECKO_CARDLOCK_PCK_PUK: + case GECKO_CARDLOCK_RCCK_PUK: // Fall through. + case GECKO_CARDLOCK_RSPCK_PUK: + this.enterDepersonalization(options); + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + } + }, + + /** + * Enter a PIN to unlock the ICC. + * + * @param password + * String containing the PIN. + * @param [optional] aid + * AID value. + */ + enterICCPIN: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_SIM_PIN, options); + Buf.writeInt32(2); + Buf.writeString(options.password); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Enter a PIN2 to unlock the ICC. + * + * @param password + * String containing the PIN2. + * @param [optional] aid + * AID value. + */ + enterICCPIN2: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_SIM_PIN2, options); + Buf.writeInt32(2); + Buf.writeString(options.password); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Requests a network personalization be deactivated. + * + * @param personlization + * One of CARD_PERSOSUBSTATE_* + * @param password + * String containing the password. + */ + enterDepersonalization: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE, options); + Buf.writeInt32(1); + Buf.writeString(options.password); + Buf.sendParcel(); + }, + + /** + * Change the current ICC PIN number. + * + * @param password + * String containing the old PIN value + * @param newPassword + * String containing the new PIN value + * @param [optional] aid + * AID value. + */ + changeICCPIN: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CHANGE_SIM_PIN, options); + Buf.writeInt32(3); + Buf.writeString(options.password); + Buf.writeString(options.newPassword); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Change the current ICC PIN2 number. + * + * @param password + * String containing the old PIN2 value + * @param newPassword + * String containing the new PIN2 value + * @param [optional] aid + * AID value. + */ + changeICCPIN2: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CHANGE_SIM_PIN2, options); + Buf.writeInt32(3); + Buf.writeString(options.password); + Buf.writeString(options.newPassword); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Supplies ICC PUK and a new PIN to unlock the ICC. + * + * @param password + * String containing the PUK value. + * @param newPassword + * String containing the new PIN value. + * @param [optional] aid + * AID value. + */ + enterICCPUK: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_SIM_PUK, options); + Buf.writeInt32(3); + Buf.writeString(options.password); + Buf.writeString(options.newPin); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Supplies ICC PUK2 and a new PIN2 to unlock the ICC. + * + * @param password + * String containing the PUK2 value. + * @param newPassword + * String containing the new PIN2 value. + * @param [optional] aid + * AID value. + */ + enterICCPUK2: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_ENTER_SIM_PUK2, options); + Buf.writeInt32(3); + Buf.writeString(options.password); + Buf.writeString(options.newPin); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Helper function for changing ICC locks. + */ + iccChangeCardLockPassword: function(options) { + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: + this.changeICCPIN(options); + break; + case GECKO_CARDLOCK_PIN2: + this.changeICCPIN2(options); + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + } + }, + + /** + * Helper function for setting the state of ICC locks. + */ + iccSetCardLockEnabled: function(options) { + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: // Fall through. + case GECKO_CARDLOCK_FDN: + options.facility = GECKO_CARDLOCK_TO_FACILITY[options.lockType]; + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + options.serviceClass = ICC_SERVICE_CLASS_VOICE | + ICC_SERVICE_CLASS_DATA | + ICC_SERVICE_CLASS_FAX; + this.setICCFacilityLock(options); + }, + + /** + * Helper function for fetching the state of ICC locks. + */ + iccGetCardLockEnabled: function(options) { + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: // Fall through. + case GECKO_CARDLOCK_FDN: + options.facility = GECKO_CARDLOCK_TO_FACILITY[options.lockType]; + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + options.password = ""; // For query no need to provide pin. + options.serviceClass = ICC_SERVICE_CLASS_VOICE | + ICC_SERVICE_CLASS_DATA | + ICC_SERVICE_CLASS_FAX; + this.queryICCFacilityLock(options); + }, + + /** + * Helper function for fetching the number of unlock retries of ICC locks. + * + * We only query the retry count when we're on the emulator. The phones do + * not support the request id and their rild doesn't return an error. + */ + iccGetCardLockRetryCount: function(options) { + if (!RILQUIRKS_HAVE_QUERY_ICC_LOCK_RETRY_COUNT) { + // Only the emulator supports this request. + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + switch (options.lockType) { + case GECKO_CARDLOCK_PIN: + case GECKO_CARDLOCK_PIN2: + case GECKO_CARDLOCK_PUK: + case GECKO_CARDLOCK_PUK2: + case GECKO_CARDLOCK_NCK: + case GECKO_CARDLOCK_NSCK: + case GECKO_CARDLOCK_CCK: // Fall through. + case GECKO_CARDLOCK_SPCK: + // TODO: Bug 1116072: identify the mapping between RIL_PERSOSUBSTATE_SIM_SIM + // @ ril.h and TS 27.007, clause 8.65 for GECKO_CARDLOCK_SPCK. + options.selCode = GECKO_CARDLOCK_TO_SEL_CODE[options.lockType]; + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + this.queryICCLockRetryCount(options); + }, + + /** + * Query ICC lock retry count. + * + * @param selCode + * One of ICC_SEL_CODE_*. + * @param serviceClass + * One of ICC_SERVICE_CLASS_*. + */ + queryICCLockRetryCount: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_GET_UNLOCK_RETRY_COUNT, options); + Buf.writeInt32(1); + Buf.writeString(options.selCode); + Buf.sendParcel(); + }, + + /** + * Query ICC facility lock. + * + * @param facility + * One of ICC_CB_FACILITY_*. + * @param password + * Password for the facility, or "" if not required. + * @param serviceClass + * One of ICC_SERVICE_CLASS_*. + * @param [optional] aid + * AID value. + */ + queryICCFacilityLock: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_QUERY_FACILITY_LOCK, options); + Buf.writeInt32(4); + Buf.writeString(options.facility); + Buf.writeString(options.password); + Buf.writeString(options.serviceClass.toString()); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Set ICC facility lock. + * + * @param facility + * One of ICC_CB_FACILITY_*. + * @param enabled + * true to enable, false to disable. + * @param password + * Password for the facility, or "" if not required. + * @param serviceClass + * One of ICC_SERVICE_CLASS_*. + * @param [optional] aid + * AID value. + */ + setICCFacilityLock: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_FACILITY_LOCK, options); + Buf.writeInt32(5); + Buf.writeString(options.facility); + Buf.writeString(options.enabled ? "1" : "0"); + Buf.writeString(options.password); + Buf.writeString(options.serviceClass.toString()); + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Request an ICC I/O operation. + * + * See TS 27.007 "restricted SIM" operation, "AT Command +CRSM". + * The sequence is in the same order as how libril reads this parcel, + * see the struct RIL_SIM_IO_v5 or RIL_SIM_IO_v6 defined in ril.h + * + * @param command + * The I/O command, one of the ICC_COMMAND_* constants. + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + * @param pathId + * String type, check the 'pathid' parameter from TS 27.007 +CRSM. + * @param p1, p2, p3 + * Arbitrary integer parameters for the command. + * @param [optional] dataWriter + * The function for writing string parameter for the ICC_COMMAND_UPDATE_RECORD. + * @param [optional] pin2 + * String containing the PIN2. + * @param [optional] aid + * AID value. + */ + iccIO: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SIM_IO, options); + Buf.writeInt32(options.command); + Buf.writeInt32(options.fileId); + Buf.writeString(options.pathId); + Buf.writeInt32(options.p1); + Buf.writeInt32(options.p2); + Buf.writeInt32(options.p3); + + // Write data. + if (options.command == ICC_COMMAND_UPDATE_RECORD && + options.dataWriter) { + options.dataWriter(options.p3); + } else { + Buf.writeString(null); + } + + // Write pin2. + if (options.command == ICC_COMMAND_UPDATE_RECORD && + options.pin2) { + Buf.writeString(options.pin2); + } else { + Buf.writeString(null); + } + + Buf.writeString(options.aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Get IMSI. + * + * @param [optional] aid + * AID value. + */ + getIMSI: function(aid) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_GET_IMSI); + Buf.writeInt32(1); + Buf.writeString(aid || this.aid); + Buf.sendParcel(); + }, + + /** + * Retrieve ICC's GID1 field. + */ + getGID1: function(options) { + options.gid1 = this.iccInfoPrivate.gid1; + this.sendChromeMessage(options); + }, + + /** + * Read UICC Phonebook contacts. + * + * @param contactType + * One of GECKO_CARDCONTACT_TYPE_*. + * @param requestId + * Request id from RadioInterfaceLayer. + */ + readICCContacts: function(options) { + if (!this.appType) { + options.errorMsg = CONTACT_ERR_REQUEST_NOT_SUPPORTED; + this.sendChromeMessage(options); + return; + } + + this.context.ICCContactHelper.readICCContacts( + this.appType, + options.contactType, + function onsuccess(contacts) { + for (let i = 0; i < contacts.length; i++) { + let contact = contacts[i]; + let pbrIndex = contact.pbrIndex || 0; + let recordIndex = pbrIndex * ICC_MAX_LINEAR_FIXED_RECORDS + contact.recordId; + contact.contactId = this.iccInfo.iccid + recordIndex; + } + // Reuse 'options' to get 'requestId' and 'contactType'. + options.contacts = contacts; + this.sendChromeMessage(options); + }.bind(this), + function onerror(errorMsg) { + options.errorMsg = errorMsg; + this.sendChromeMessage(options); + }.bind(this)); + }, + + /** + * Update UICC Phonebook. + * + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param contact The contact will be updated. + * @param pin2 PIN2 is required for updating FDN. + * @param requestId Request id from RadioInterfaceLayer. + */ + updateICCContact: function(options) { + let onsuccess = function onsuccess(updatedContact) { + let recordIndex = + updatedContact.pbrIndex * ICC_MAX_LINEAR_FIXED_RECORDS + updatedContact.recordId; + updatedContact.contactId = this.iccInfo.iccid + recordIndex; + options.contact = updatedContact; + // Reuse 'options' to get 'requestId' and 'contactType'. + this.sendChromeMessage(options); + }.bind(this); + + let onerror = function onerror(errorMsg) { + options.errorMsg = errorMsg; + this.sendChromeMessage(options); + }.bind(this); + + if (!this.appType || !options.contact) { + onerror(CONTACT_ERR_REQUEST_NOT_SUPPORTED ); + return; + } + + let contact = options.contact; + let iccid = this.iccInfo.iccid; + let isValidRecordId = false; + if (typeof contact.contactId === "string" && + contact.contactId.startsWith(iccid)) { + let recordIndex = contact.contactId.substring(iccid.length); + contact.pbrIndex = Math.floor(recordIndex / ICC_MAX_LINEAR_FIXED_RECORDS); + contact.recordId = recordIndex % ICC_MAX_LINEAR_FIXED_RECORDS; + isValidRecordId = contact.recordId > 0 && contact.recordId < 0xff; + } + + if (DEBUG) { + this.context.debug("Update ICC Contact " + JSON.stringify(contact)); + } + + let ICCContactHelper = this.context.ICCContactHelper; + // If contact has 'recordId' property, updates corresponding record. + // If not, inserts the contact into a free record. + if (isValidRecordId) { + ICCContactHelper.updateICCContact( + this.appType, options.contactType, contact, options.pin2, onsuccess, onerror); + } else { + ICCContactHelper.addICCContact( + this.appType, options.contactType, contact, options.pin2, onsuccess, onerror); + } + }, + + /** + * Check if operator name needs to be overriden by current voiceRegistrationState + * , EFOPL and EFPNN. See 3GPP TS 31.102 clause 4.2.58 EFPNN and 4.2.59 EFOPL + * for detail. + * + * @return true if operator name is overridden, false otherwise. + */ + overrideICCNetworkName: function() { + if (!this.operator) { + return false; + } + + // We won't get network name using PNN and OPL if voice registration isn't + // ready. + if (!this.voiceRegistrationState.cell || + this.voiceRegistrationState.cell.gsmLocationAreaCode == -1) { + return false; + } + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + let networkName = ICCUtilsHelper.getNetworkNameFromICC( + this.operator.mcc, + this.operator.mnc, + this.voiceRegistrationState.cell.gsmLocationAreaCode); + + if (!networkName) { + return false; + } + + if (DEBUG) { + this.context.debug("Operator names will be overriden: " + + "longName = " + networkName.fullName + ", " + + "shortName = " + networkName.shortName); + } + + this.operator.longName = networkName.fullName; + this.operator.shortName = networkName.shortName; + + this._sendNetworkInfoMessage(NETWORK_INFO_OPERATOR, this.operator); + return true; + }, + + /** + * Request the phone's radio to be enabled or disabled. + * + * @param enabled + * Boolean indicating the desired state. + */ + setRadioEnabled: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_RADIO_POWER, options); + Buf.writeInt32(1); + Buf.writeInt32(options.enabled ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Query call waiting status. + * + */ + queryCallWaiting: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_QUERY_CALL_WAITING, options); + Buf.writeInt32(1); + // As per 3GPP TS 24.083, section 1.6 UE doesn't need to send service + // class parameter in call waiting interrogation to network. + Buf.writeInt32(ICC_SERVICE_CLASS_NONE); + Buf.sendParcel(); + }, + + /** + * Set call waiting status. + * + * @param enabled + * Boolean indicating the desired waiting status. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + */ + setCallWaiting: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_CALL_WAITING, options); + Buf.writeInt32(2); + Buf.writeInt32(options.enabled ? 1 : 0); + Buf.writeInt32(options.serviceClass); + Buf.sendParcel(); + }, + + /** + * Queries current CLIP status. + */ + queryCLIP: function(options) { + this.context.Buf.simpleRequest(REQUEST_QUERY_CLIP, options); + }, + + /** + * Queries current CLIR status. + * + */ + getCLIR: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_CLIR, options); + }, + + /** + * Enables or disables the presentation of the calling line identity (CLI) to + * the called party when originating a call. + * + * @param options.clirMode + * One of the CLIR_* constants. + */ + setCLIR: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_CLIR, options); + Buf.writeInt32(1); + Buf.writeInt32(options.clirMode); + Buf.sendParcel(); + }, + + /** + * Set screen state. + * + * @param on + * Boolean indicating whether the screen should be on or off. + */ + setScreenState: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SCREEN_STATE); + Buf.writeInt32(1); + Buf.writeInt32(options.on ? 1 : 0); + Buf.sendParcel(); + }, + + getVoiceRegistrationState: function() { + this.context.Buf.simpleRequest(REQUEST_VOICE_REGISTRATION_STATE); + }, + + getVoiceRadioTechnology: function() { + this.context.Buf.simpleRequest(REQUEST_VOICE_RADIO_TECH); + }, + + getDataRegistrationState: function() { + this.context.Buf.simpleRequest(REQUEST_DATA_REGISTRATION_STATE); + }, + + getOperator: function() { + this.context.Buf.simpleRequest(REQUEST_OPERATOR); + }, + + /** + * Set the preferred network type. + * + * @param options An object contains a valid value of + * RIL_PREFERRED_NETWORK_TYPE_TO_GECKO as its `type` attribute. + */ + setPreferredNetworkType: function(options) { + let networkType = options.type; + if (networkType < 0 || networkType >= RIL_PREFERRED_NETWORK_TYPE_TO_GECKO.length) { + options.errorMsg = GECKO_ERROR_INVALID_PARAMETER; + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_PREFERRED_NETWORK_TYPE, options); + Buf.writeInt32(1); + Buf.writeInt32(networkType); + Buf.sendParcel(); + }, + + /** + * Get the preferred network type. + */ + getPreferredNetworkType: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_PREFERRED_NETWORK_TYPE, options); + }, + + /** + * Request neighboring cell ids in GSM network. + */ + getNeighboringCellIds: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_NEIGHBORING_CELL_IDS, options); + }, + + /** + * Request all of the current cell information known to the radio. + */ + getCellInfoList: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_CELL_INFO_LIST, options); + }, + + /** + * Request various states about the network. + */ + requestNetworkInfo: function() { + if (this._processingNetworkInfo) { + if (DEBUG) { + this.context.debug("Network info requested, but we're already " + + "requesting network info."); + } + this._needRepollNetworkInfo = true; + return; + } + + if (DEBUG) this.context.debug("Requesting network info"); + + this._processingNetworkInfo = true; + this.getVoiceRegistrationState(); + this.getDataRegistrationState(); //TODO only GSM + this.getOperator(); + this.getNetworkSelectionMode(); + this.getSignalStrength(); + }, + + /** + * Get the available networks + */ + getAvailableNetworks: function(options) { + if (DEBUG) this.context.debug("Getting available networks"); + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_QUERY_AVAILABLE_NETWORKS, options); + Buf.sendParcel(); + }, + + /** + * Request the radio's network selection mode + */ + getNetworkSelectionMode: function() { + if (DEBUG) this.context.debug("Getting network selection mode"); + this.context.Buf.simpleRequest(REQUEST_QUERY_NETWORK_SELECTION_MODE); + }, + + /** + * Tell the radio to automatically choose a voice/data network + */ + selectNetworkAuto: function(options) { + if (DEBUG) this.context.debug("Setting automatic network selection"); + this.context.Buf.simpleRequest(REQUEST_SET_NETWORK_SELECTION_AUTOMATIC, options); + }, + + /** + * Set the roaming preference mode + */ + setRoamingPreference: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CDMA_SET_ROAMING_PREFERENCE, options); + Buf.writeInt32(1); + Buf.writeInt32(options.mode); + Buf.sendParcel(); + }, + + /** + * Get the roaming preference mode + */ + queryRoamingPreference: function(options) { + this.context.Buf.simpleRequest(REQUEST_CDMA_QUERY_ROAMING_PREFERENCE, options); + }, + + /** + * Set the voice privacy mode + */ + setVoicePrivacyMode: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE, options); + Buf.writeInt32(1); + Buf.writeInt32(options.enabled ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Get the voice privacy mode + */ + queryVoicePrivacyMode: function(options) { + this.context.Buf.simpleRequest(REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE, options); + }, + + /** + * Open Logical UICC channel (aid) for Secure Element access + */ + iccOpenChannel: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SIM_OPEN_CHANNEL, options); + Buf.writeString(options.aid); + Buf.sendParcel(); + }, + + /** + * Exchange APDU data on an open Logical UICC channel + */ + iccExchangeAPDU: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SIM_TRANSMIT_APDU_CHANNEL, options); + Buf.writeInt32(options.channel); + Buf.writeInt32(options.apdu.cla); + Buf.writeInt32(options.apdu.command); + Buf.writeInt32(options.apdu.p1); + Buf.writeInt32(options.apdu.p2); + Buf.writeInt32(options.apdu.p3); + Buf.writeString(options.apdu.data); + Buf.sendParcel(); + }, + + /** + * Close Logical UICC channel + */ + iccCloseChannel: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SIM_CLOSE_CHANNEL, options); + Buf.writeInt32(1); + Buf.writeInt32(options.channel); + Buf.sendParcel(); + }, + + /** + * Get UICC service state + */ + getIccServiceState: function(options) { + switch (options.service) { + case GECKO_CARDSERVICE_FDN: + let ICCUtilsHelper = this.context.ICCUtilsHelper; + options.result = ICCUtilsHelper.isICCServiceAvailable("FDN"); + break; + default: + options.errorMsg = GECKO_ERROR_REQUEST_NOT_SUPPORTED; + break; + } + this.sendChromeMessage(options); + }, + + /** + * Enable/Disable UICC subscription + */ + setUiccSubscription: function(options) { + if (DEBUG) { + this.context.debug("setUiccSubscription: " + JSON.stringify(options)); + } + + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_UICC_SUBSCRIPTION, options); + Buf.writeInt32(this.context.clientId); + Buf.writeInt32(options.appIndex); + Buf.writeInt32(this.context.clientId); + Buf.writeInt32(options.enabled ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Tell the radio to choose a specific voice/data network + */ + selectNetwork: function(options) { + if (DEBUG) { + this.context.debug("Setting manual network selection: " + + options.mcc + ", " + options.mnc); + } + + let numeric = (options.mcc && options.mnc) ? options.mcc + options.mnc : null; + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_NETWORK_SELECTION_MANUAL, options); + Buf.writeString(numeric); + Buf.sendParcel(); + }, + + /** + * Get the signal strength. + */ + getSignalStrength: function() { + this.context.Buf.simpleRequest(REQUEST_SIGNAL_STRENGTH); + }, + + getDeviceIdentity: function() { + this.deviceIdentities || this.context.Buf.simpleRequest(REQUEST_DEVICE_IDENTITY); + }, + + getBasebandVersion: function() { + this.context.Buf.simpleRequest(REQUEST_BASEBAND_VERSION); + }, + + sendExitEmergencyCbModeRequest: function(options) { + this.context.Buf.simpleRequest(REQUEST_EXIT_EMERGENCY_CALLBACK_MODE, options); + }, + + getCdmaSubscription: function() { + this.context.Buf.simpleRequest(REQUEST_CDMA_SUBSCRIPTION); + }, + + exitEmergencyCbMode: function(options) { + // The function could be called by an API from RadioInterfaceLayer or by + // ril_worker itself. From ril_worker, we won't pass the parameter + // 'options'. In this case, it is marked as internal. + if (!options) { + options = {internal: true}; + } + this._cancelEmergencyCbModeTimeout(); + this.sendExitEmergencyCbModeRequest(options); + }, + + /** + * Dial a non-emergency number. + * + * @param isEmergency + * Whether the number is an emergency number. + * @param number + * String containing the number to dial. + * @param clirMode + * Integer for showing/hidding the caller Id to the called party. + * @param uusInfo + * Integer doing something XXX TODO + */ + dial: function(options) { + if (options.isEmergency) { + options.request = RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL ? + REQUEST_DIAL_EMERGENCY_CALL : REQUEST_DIAL; + + } else { + options.request = REQUEST_DIAL; + + // Exit emergency callback mode when user dial a non-emergency call. + if (this._isInEmergencyCbMode) { + this.exitEmergencyCbMode(); + } + } + + this.telephonyRequestQueue.push(options.request, () => { + let Buf = this.context.Buf; + Buf.newParcel(options.request, options); + Buf.writeString(options.number); + Buf.writeInt32(options.clirMode || 0); + Buf.writeInt32(options.uusInfo || 0); + // TODO Why do we need this extra 0? It was put it in to make this + // match the format of the binary message. + Buf.writeInt32(0); + Buf.sendParcel(); + }); + }, + + /** + * CDMA flash. + * + * @param featureStr (optional) + * Dialing number when the command is used for three-way-calling + */ + cdmaFlash: function(options) { + let Buf = this.context.Buf; + options.request = REQUEST_CDMA_FLASH; + Buf.newParcel(options.request, options); + Buf.writeString(options.featureStr || ""); + Buf.sendParcel(); + }, + + /** + * Hang up the phone. + * + * @param callIndex + * Call index (1-based) as reported by REQUEST_GET_CURRENT_CALLS. + */ + hangUpCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_HANGUP, () => { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_HANGUP, options); + Buf.writeInt32(1); + Buf.writeInt32(options.callIndex); + Buf.sendParcel(); + }); + }, + + hangUpForeground: function(options) { + this.telephonyRequestQueue.push(REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND, () => { + this.context.Buf.simpleRequest(REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND, + options); + }); + }, + + hangUpBackground: function(options) { + this.telephonyRequestQueue.push(REQUEST_HANGUP_WAITING_OR_BACKGROUND, () => { + this.context.Buf.simpleRequest(REQUEST_HANGUP_WAITING_OR_BACKGROUND, + options); + }); + }, + + switchActiveCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, () => { + this.context.Buf.simpleRequest(REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, + options); + }); + }, + + udub: function(options) { + this.telephonyRequestQueue.push(REQUEST_UDUB, () => { + this.context.Buf.simpleRequest(REQUEST_UDUB, options); + }); + }, + + answerCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_ANSWER, () => { + this.context.Buf.simpleRequest(REQUEST_ANSWER, options); + }); + }, + + conferenceCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_CONFERENCE, () => { + this.context.Buf.simpleRequest(REQUEST_CONFERENCE, options); + }); + }, + + separateCall: function(options) { + this.telephonyRequestQueue.push(REQUEST_SEPARATE_CONNECTION, () => { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SEPARATE_CONNECTION, options); + Buf.writeInt32(1); + Buf.writeInt32(options.callIndex); + Buf.sendParcel(); + }); + }, + + /** + * Get current calls. + */ + getCurrentCalls: function(options) { + this.telephonyRequestQueue.push(REQUEST_GET_CURRENT_CALLS, () => { + this.context.Buf.simpleRequest(REQUEST_GET_CURRENT_CALLS, options); + }); + }, + + /** + * Mute or unmute the radio. + * + * @param mute + * Boolean to indicate whether to mute or unmute the radio. + */ + setMute: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_MUTE); + Buf.writeInt32(1); + Buf.writeInt32(options.muted ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Send an SMS. + * + * The `options` parameter object should contain the following attributes: + * + * @param number + * String containing the recipient number. + * @param body + * String containing the message text. + * @param envelopeId + * Numeric value identifying the sms request. + */ + sendSMS: function(options) { + options.langIndex = options.langIndex || PDU_NL_IDENTIFIER_DEFAULT; + options.langShiftIndex = options.langShiftIndex || PDU_NL_IDENTIFIER_DEFAULT; + + if (!options.segmentSeq) { + // Fist segment to send + options.segmentSeq = 1; + options.body = options.segments[0].body; + options.encodedBodyLength = options.segments[0].encodedBodyLength; + } + + let Buf = this.context.Buf; + if (this._isCdma) { + Buf.newParcel(REQUEST_CDMA_SEND_SMS, options); + this.context.CdmaPDUHelper.writeMessage(options); + } else { + Buf.newParcel(REQUEST_SEND_SMS, options); + Buf.writeInt32(2); + Buf.writeString(options.SMSC); + this.context.GsmPDUHelper.writeMessage(options); + } + Buf.sendParcel(); + }, + + /** + * Acknowledge the receipt and handling of an SMS. + * + * @param success + * Boolean indicating whether the message was successfuly handled. + * @param cause + * SMS_* constant indicating the reason for unsuccessful handling. + */ + acknowledgeGsmSms: function(success, cause) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SMS_ACKNOWLEDGE); + Buf.writeInt32(2); + Buf.writeInt32(success ? 1 : 0); + Buf.writeInt32(cause); + Buf.sendParcel(); + }, + + /** + * Acknowledge the receipt and handling of an SMS. + * + * @param success + * Boolean indicating whether the message was successfuly handled. + */ + ackSMS: function(options) { + if (options.result == PDU_FCS_RESERVED) { + return; + } + if (this._isCdma) { + this.acknowledgeCdmaSms(options.result == PDU_FCS_OK, options.result); + } else { + this.acknowledgeGsmSms(options.result == PDU_FCS_OK, options.result); + } + }, + + /** + * Acknowledge the receipt and handling of a CDMA SMS. + * + * @param success + * Boolean indicating whether the message was successfuly handled. + * @param cause + * SMS_* constant indicating the reason for unsuccessful handling. + */ + acknowledgeCdmaSms: function(success, cause) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CDMA_SMS_ACKNOWLEDGE); + Buf.writeInt32(success ? 0 : 1); + Buf.writeInt32(cause); + Buf.sendParcel(); + }, + + /** + * Update received MWI into EF_MWIS. + */ + updateMwis: function(options) { + if (this.context.ICCUtilsHelper.isICCServiceAvailable("MWIS")) { + this.context.SimRecordHelper.updateMWIS(options.mwi); + } + }, + + /** + * Report SMS storage status to modem. + */ + _updateSmsMemoryStatus: function() { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_REPORT_SMS_MEMORY_STATUS); + Buf.writeInt32(1); + Buf.writeInt32(this.smsStorageAvailable ? 1 : 0); + Buf.sendParcel(); + }, + + reportSmsMemoryStatus: function(options) { + this.pendingToReportSmsMemoryStatus = true; + this.smsStorageAvailable = options.isAvailable; + this._updateSmsMemoryStatus(); + }, + + setCellBroadcastDisabled: function(options) { + this.cellBroadcastDisabled = options.disabled; + + // If |this.mergedCellBroadcastConfig| is null, either we haven't finished + // reading required SIM files, or no any channel is ever configured. In + // the former case, we'll call |this.updateCellBroadcastConfig()| later + // with correct configs; in the latter case, we don't bother resetting CB + // to disabled again. + if (this.mergedCellBroadcastConfig) { + this.updateCellBroadcastConfig(); + } + }, + + setCellBroadcastSearchList: function(options) { + let getSearchListStr = function(aSearchList) { + if (typeof aSearchList === "string" || aSearchList instanceof String) { + return aSearchList; + } + + // TODO: Set search list for CDMA/GSM individually. Bug 990926 + let prop = this._isCdma ? "cdma" : "gsm"; + + return aSearchList && aSearchList[prop]; + }.bind(this); + + try { + let str = getSearchListStr(options.searchList); + this.cellBroadcastConfigs.MMI = this._convertCellBroadcastSearchList(str); + } catch (e) { + if (DEBUG) { + this.context.debug("Invalid Cell Broadcast search list: " + e); + } + options.errorMsg = GECKO_ERROR_UNSPECIFIED_ERROR; + } + + this.sendChromeMessage(options); + if (options.errorMsg) { + return; + } + + this._mergeAllCellBroadcastConfigs(); + }, + + updateCellBroadcastConfig: function() { + let activate = !this.cellBroadcastDisabled && + (this.mergedCellBroadcastConfig != null) && + (this.mergedCellBroadcastConfig.length > 0); + if (activate) { + this.setSmsBroadcastConfig(this.mergedCellBroadcastConfig); + } else { + // It's unnecessary to set config first if we're deactivating. + this.setSmsBroadcastActivation(false); + } + }, + + setGsmSmsBroadcastConfig: function(config) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_GSM_SET_BROADCAST_SMS_CONFIG); + + let numConfigs = config ? config.length / 2 : 0; + Buf.writeInt32(numConfigs); + for (let i = 0; i < config.length;) { + // convert [from, to) to [from, to - 1] + Buf.writeInt32(config[i++]); + Buf.writeInt32(config[i++] - 1); + Buf.writeInt32(0x00); + Buf.writeInt32(0xFF); + Buf.writeInt32(1); + } + + Buf.sendParcel(); + }, + + /** + * Send CDMA SMS broadcast config. + * + * @see 3GPP2 C.R1001 Sec. 9.2 and 9.3 + */ + setCdmaSmsBroadcastConfig: function(config) { + let Buf = this.context.Buf; + // |config| is an array of half-closed range: [[from, to), [from, to), ...]. + // It will be further decomposed, ex: [1, 4) => 1, 2, 3. + Buf.newParcel(REQUEST_CDMA_SET_BROADCAST_SMS_CONFIG); + + let numConfigs = 0; + for (let i = 0; i < config.length; i += 2) { + numConfigs += (config[i+1] - config[i]); + } + + Buf.writeInt32(numConfigs); + for (let i = 0; i < config.length;) { + let begin = config[i++]; + let end = config[i++]; + + for (let j = begin; j < end; ++j) { + Buf.writeInt32(j); + Buf.writeInt32(0); // Language Indicator: Unknown or unspecified. + Buf.writeInt32(1); + } + } + + Buf.sendParcel(); + }, + + setSmsBroadcastConfig: function(config) { + if (this._isCdma) { + this.setCdmaSmsBroadcastConfig(config); + } else { + this.setGsmSmsBroadcastConfig(config); + } + }, + + setSmsBroadcastActivation: function(activate) { + let parcelType = this._isCdma ? REQUEST_CDMA_SMS_BROADCAST_ACTIVATION : + REQUEST_GSM_SMS_BROADCAST_ACTIVATION; + let Buf = this.context.Buf; + Buf.newParcel(parcelType); + Buf.writeInt32(1); + // See hardware/ril/include/telephony/ril.h, 0 - Activate, 1 - Turn off. + Buf.writeInt32(activate ? 0 : 1); + Buf.sendParcel(); + }, + + /** + * Start a DTMF Tone. + * + * @param dtmfChar + * DTMF signal to send, 0-9, *, + + */ + startTone: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_DTMF_START, options); + Buf.writeString(options.dtmfChar); + Buf.sendParcel(); + }, + + stopTone: function() { + this.context.Buf.simpleRequest(REQUEST_DTMF_STOP); + }, + + /** + * Send a DTMF tone. + * + * @param dtmfChar + * DTMF signal to send, 0-9, *, + + */ + sendTone: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_DTMF); + Buf.writeString(options.dtmfChar); + Buf.sendParcel(); + }, + + /** + * Get the Short Message Service Center address. + */ + getSmscAddress: function(options) { + this.context.Buf.simpleRequest(REQUEST_GET_SMSC_ADDRESS, options); + }, + + /** + * Set the Short Message Service Center address. + * + * @param smscAddress + * Number part of the SMSC address. + * @param typeOfNumber + * Type of number in integer, as defined in + * |Table 10.5.118: Called party BCD number| of 3GPP TS 24.008. + * @param numberPlanIdentification + * The index of number plan identification value in + * CALLED_PARTY_BCD_NPI array. + */ + setSmscAddress: function(options) { + let ton = options.typeOfNumber; + let npi = CALLED_PARTY_BCD_NPI[options.numberPlanIdentification]; + + // If any of the mandatory arguments is not available, return an error + // immediately. + if (ton === undefined || npi === undefined || !options.smscAddress) { + options.errorMsg = GECKO_ERROR_INVALID_PARAMETER; + this.sendChromeMessage(options); + return; + } + + // Remove all illegal characters in the number string for user-input fault + // tolerance. + let numStart = options.smscAddress[0] === "+" ? 1 : 0; + let number = options.smscAddress.substring(0, numStart) + + options.smscAddress.substring(numStart) + .replace(/[^0-9*#abc]/ig, ""); + + // If the filtered number is an empty string, return an error immediately. + if (number.length === 0) { + options.errorMsg = GECKO_ERROR_INVALID_PARAMETER; + this.sendChromeMessage(options); + return; + } + + // Init parcel. + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_SMSC_ADDRESS, options); + + // +---+-----------+---------------+ + // | 1 | TON | NPI | + // +---+-----------+---------------+ + let tosca = (0x1 << 7) + (ton << 4) + npi; + if (RILQUIRKS_SMSC_ADDRESS_FORMAT === "pdu") { + let pduHelper = this.context.GsmPDUHelper; + + // Remove the preceding '+', and covert the special BCD digits defined in + // |Called party BCD number| of 3GPP TS 24.008 to corresponding + // hexadecimal values (refer the following table). + // + // +=========+=======+=====+ + // | value | digit | hex | + // +======================== + // | 1 0 1 0 | * | 0xA | + // | 1 0 1 1 | # | 0xB | + // | 1 1 0 0 | a | 0xC | + // | 1 1 0 1 | b | 0xD | + // | 1 1 1 0 | c | 0xE | + // +=========+=======+=====+ + // + // The replace order is reversed intentionally, because if the digits are + // replaced in ascending order, "#" will be converted to "b" and then be + // converted again to "d", which generates incorrect result. + let pureNumber = number.substring(numStart) + .replace(/c/ig, "e") + .replace(/b/ig, "d") + .replace(/a/ig, "c") + .replace(/\#/g, "b") + .replace(/\*/g, "a"); + + // address length and string length + let length = Math.ceil(pureNumber.length / 2) + 1; // +1 octet for TOA + let strlen = length * 2 + 2; // +2 semi-octets for length octet + + Buf.writeInt32(strlen); + pduHelper.writeHexOctet(length); + pduHelper.writeHexOctet(tosca); + pduHelper.writeSwappedNibbleBCD(pureNumber); + Buf.writeStringDelimiter(strlen); + } else /* RILQUIRKS_SMSC_ADDRESS_FORMAT === "text" */ { + let sca; + sca = '"' + number + '"' + ',' + tosca; + Buf.writeString(sca); + } + + Buf.sendParcel(); + }, + + /** + * Setup a data call. + * + * @param radioTech + * Integer to indicate radio technology. + * DATACALL_RADIOTECHNOLOGY_CDMA => CDMA. + * DATACALL_RADIOTECHNOLOGY_GSM => GSM. + * @param apn + * String containing the name of the APN to connect to. + * @param user + * String containing the username for the APN. + * @param passwd + * String containing the password for the APN. + * @param chappap + * Integer containing CHAP/PAP auth type. + * DATACALL_AUTH_NONE => PAP and CHAP is never performed. + * DATACALL_AUTH_PAP => PAP may be performed. + * DATACALL_AUTH_CHAP => CHAP may be performed. + * DATACALL_AUTH_PAP_OR_CHAP => PAP / CHAP may be performed. + * @param pdptype + * String containing PDP type to request. ("IP", "IPV6", ...) + */ + setupDataCall: function(options) { + // From ./hardware/ril/include/telephony/ril.h: + // ((const char **)data)[0] Radio technology to use: 0-CDMA, 1-GSM/UMTS, 2... + // for values above 2 this is RIL_RadioTechnology + 2. + // + // From frameworks/base/telephony/java/com/android/internal/telephony/DataConnection.java: + // if the mRilVersion < 6, radio technology must be GSM/UMTS or CDMA. + // Otherwise, it must be + 2 + // + // See also bug 901232 and 867873 + let radioTech = options.radioTech + 2; + let Buf = this.context.Buf; + let token = Buf.newParcel(REQUEST_SETUP_DATA_CALL, options); + Buf.writeInt32(7); + Buf.writeString(radioTech.toString()); + Buf.writeString(DATACALL_PROFILE_DEFAULT.toString()); + Buf.writeString(options.apn); + Buf.writeString(options.user); + Buf.writeString(options.passwd); + Buf.writeString(options.chappap.toString()); + Buf.writeString(options.pdptype); + Buf.sendParcel(); + return token; + }, + + /** + * Deactivate a data call. + * + * @param cid + * String containing CID. + * @param reason + * One of DATACALL_DEACTIVATE_* constants. + */ + deactivateDataCall: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_DEACTIVATE_DATA_CALL, options); + Buf.writeInt32(2); + Buf.writeString(options.cid.toString()); + Buf.writeString(options.reason !== undefined ? + options.reason.toString() : + DATACALL_DEACTIVATE_NO_REASON.toString()); + Buf.sendParcel(); + }, + + /** + * Get a list of data calls. + */ + getDataCallList: function(options) { + this.context.Buf.simpleRequest(REQUEST_DATA_CALL_LIST, options); + }, + + _attachDataRegistration: false, + /** + * Manually attach/detach data registration. + * + * @param attach + * Boolean value indicating attach or detach. + */ + setDataRegistration: function(options) { + this._attachDataRegistration = options.attach; + + if (RILQUIRKS_DATA_REGISTRATION_ON_DEMAND) { + let request = options.attach ? RIL_REQUEST_GPRS_ATTACH : + RIL_REQUEST_GPRS_DETACH; + this.context.Buf.simpleRequest(request, options); + return; + } else if (RILQUIRKS_SUBSCRIPTION_CONTROL && options.attach) { + this.context.Buf.simpleRequest(REQUEST_SET_DATA_SUBSCRIPTION, options); + return; + } + + // We don't really send a request to rild, so instantly reply success to + // RadioInterfaceLayer. + this.sendChromeMessage(options); + }, + + /** + * Get failure casue code for the most recently failed PDP context. + */ + getFailCause: function(options) { + this.context.Buf.simpleRequest(REQUEST_LAST_CALL_FAIL_CAUSE, options); + }, + + /** + * Send USSD. + * + * @param ussd + * String containing the USSD code. + */ + sendUSSD: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SEND_USSD, options); + Buf.writeString(options.ussd); + Buf.sendParcel(); + }, + + /** + * Cancel pending USSD. + */ + cancelUSSD: function(options) { + this.context.Buf.simpleRequest(REQUEST_CANCEL_USSD, options); + }, + + /** + * Queries current call forward rules. + * + * @param reason + * One of CALL_FORWARD_REASON_* constants. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + * @param number + * Phone number of forwarding address. + */ + queryCallForwardStatus: function(options) { + let Buf = this.context.Buf; + let number = options.number || ""; + Buf.newParcel(REQUEST_QUERY_CALL_FORWARD_STATUS, options); + Buf.writeInt32(CALL_FORWARD_ACTION_QUERY_STATUS); + Buf.writeInt32(options.reason); + Buf.writeInt32(options.serviceClass || ICC_SERVICE_CLASS_NONE); + Buf.writeInt32(this._toaFromString(number)); + Buf.writeString(number); + Buf.writeInt32(0); + Buf.sendParcel(); + }, + + /** + * Configures call forward rule. + * + * @param action + * One of CALL_FORWARD_ACTION_* constants. + * @param reason + * One of CALL_FORWARD_REASON_* constants. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + * @param number + * Phone number of forwarding address. + * @param timeSeconds + * Time in seconds to wait beforec all is forwarded. + */ + setCallForward: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_SET_CALL_FORWARD, options); + Buf.writeInt32(options.action); + Buf.writeInt32(options.reason); + Buf.writeInt32(options.serviceClass); + Buf.writeInt32(this._toaFromString(options.number)); + Buf.writeString(options.number); + Buf.writeInt32(options.timeSeconds); + Buf.sendParcel(); + }, + + /** + * Queries current call barring rules. + * + * @param program + * One of CALL_BARRING_PROGRAM_* constants. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + */ + queryCallBarringStatus: function(options) { + options.facility = CALL_BARRING_PROGRAM_TO_FACILITY[options.program]; + options.password = ""; // For query no need to provide it. + + // For some operators, querying specific serviceClass doesn't work. We use + // serviceClass 0 instead, and then process the response to extract the + // answer for queryServiceClass. + options.queryServiceClass = options.serviceClass; + options.serviceClass = 0; + + this.queryICCFacilityLock(options); + }, + + /** + * Configures call barring rule. + * + * @param program + * One of CALL_BARRING_PROGRAM_* constants. + * @param enabled + * Enable or disable the call barring. + * @param password + * Barring password. + * @param serviceClass + * One of ICC_SERVICE_CLASS_* constants. + */ + setCallBarring: function(options) { + options.facility = CALL_BARRING_PROGRAM_TO_FACILITY[options.program]; + this.setICCFacilityLock(options); + }, + + /** + * Change call barring facility password. + * + * @param pin + * Old password. + * @param newPin + * New password. + */ + changeCallBarringPassword: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_CHANGE_BARRING_PASSWORD, options); + Buf.writeInt32(3); + // Set facility to ICC_CB_FACILITY_BA_ALL by following TS.22.030 clause + // 6.5.4 and Table B.1. + Buf.writeString(ICC_CB_FACILITY_BA_ALL); + Buf.writeString(options.pin); + Buf.writeString(options.newPin); + Buf.sendParcel(); + }, + + /** + * Handle STK CALL_SET_UP request. + * + * @param hasConfirmed + * Does use have confirmed the call requested from ICC? + */ + stkHandleCallSetup: function(options) { + let Buf = this.context.Buf; + Buf.newParcel(REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM); + Buf.writeInt32(1); + Buf.writeInt32(options.hasConfirmed ? 1 : 0); + Buf.sendParcel(); + }, + + /** + * Send STK Profile Download. + * + * @param profile Profile supported by ME. + */ + sendStkTerminalProfile: function(profile) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + Buf.newParcel(REQUEST_STK_SET_PROFILE); + Buf.writeInt32(profile.length * 2); + for (let i = 0; i < profile.length; i++) { + GsmPDUHelper.writeHexOctet(profile[i]); + } + Buf.writeInt32(0); + Buf.sendParcel(); + }, + + /** + * Send STK terminal response. + * + * @param command + * @param deviceIdentities + * @param resultCode + * @param [optional] additionalInformation + * @param [optional] itemIdentifier + * @param [optional] input + * @param [optional] isYesNo + * @param [optional] hasConfirmed + * @param [optional] localInfo + * @param [optional] timer + */ + sendStkTerminalResponse: function(response) { + if (response.hasConfirmed !== undefined) { + this.stkHandleCallSetup(response); + return; + } + + let Buf = this.context.Buf; + let ComprehensionTlvHelper = this.context.ComprehensionTlvHelper; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let command = response.command; + Buf.newParcel(REQUEST_STK_SEND_TERMINAL_RESPONSE); + + // 1st mark for Parcel size + Buf.startCalOutgoingSize(function(size) { + // Parcel size is in string length, which costs 2 uint8 per char. + Buf.writeInt32(size / 2); + }); + + // Command Details + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_COMMAND_DETAILS | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(3); + if (command) { + GsmPDUHelper.writeHexOctet(command.commandNumber); + GsmPDUHelper.writeHexOctet(command.typeOfCommand); + GsmPDUHelper.writeHexOctet(command.commandQualifier); + } else { + GsmPDUHelper.writeHexOctet(0x00); + GsmPDUHelper.writeHexOctet(0x00); + GsmPDUHelper.writeHexOctet(0x00); + } + + // Device Identifier + // According to TS102.223/TS31.111 section 6.8 Structure of + // TERMINAL RESPONSE, "For all SIMPLE-TLV objects with Min=N, + // the ME should set the CR(comprehension required) flag to + // comprehension not required.(CR=0)" + // Since DEVICE_IDENTITIES and DURATION TLVs have Min=N, + // the CR flag is not set. + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DEVICE_ID); + GsmPDUHelper.writeHexOctet(2); + GsmPDUHelper.writeHexOctet(STK_DEVICE_ID_ME); + GsmPDUHelper.writeHexOctet(STK_DEVICE_ID_SIM); + + // Result + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_RESULT | + COMPREHENSIONTLV_FLAG_CR); + if ("additionalInformation" in response) { + // In |12.12 Result| TS 11.14, the length of additional information is + // varied and all possible values are addressed in 12.12.1-11 of TS 11.14 + // and 8.12.1-13 in TS 31.111. + // However, + // 1. Only SEND SS requires info with more than 1 octet. + // 2. In rild design, SEND SS is expected to be handled by modem and + // UNSOLICITED_STK_EVENT_NOTIFY will be sent to application layer to + // indicate appropriate messages to users. TR is not required in this + // case. + // Hence, we simplify the structure of |additionalInformation| to a + // numeric value instead of a octet array. + GsmPDUHelper.writeHexOctet(2); + GsmPDUHelper.writeHexOctet(response.resultCode); + GsmPDUHelper.writeHexOctet(response.additionalInformation); + } else { + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(response.resultCode); + } + + // Item Identifier + if (response.itemIdentifier != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_ITEM_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(response.itemIdentifier); + } + + // No need to process Text data if user requests help information. + if (response.resultCode != STK_RESULT_HELP_INFO_REQUIRED) { + let text; + let coding = command.options.isUCS2 ? + STK_TEXT_CODING_UCS2 : + (command.options.isPacked ? + STK_TEXT_CODING_GSM_7BIT_PACKED : + STK_TEXT_CODING_GSM_8BIT); + if (response.isYesNo !== undefined) { + // Tag: GET_INKEY + // When the ME issues a successful TERMINAL RESPONSE for a GET INKEY + // ("Yes/No") command with command qualifier set to "Yes/No", it shall + // supply the value '01' when the answer is "positive" and the value + // '00' when the answer is "negative" in the Text string data object. + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TEXT_STRING | + COMPREHENSIONTLV_FLAG_CR); + // Length: 2 + GsmPDUHelper.writeHexOctet(2); + // Value: Coding, Yes/No. + GsmPDUHelper.writeHexOctet(coding); + GsmPDUHelper.writeHexOctet(response.isYesNo ? 0x01 : 0x00); + } else { + if (response.input !== undefined) { + ComprehensionTlvHelper.writeTextStringTlv(response.input, coding); + } + } + } + + // Duration + if (response.resultCode === STK_RESULT_NO_RESPONSE_FROM_USER) { + // In TS102 223, 6.4.2 GET INKEY, "if the UICC requests a variable timeout, + // the terminal shall wait until either the user enters a single character + // or the timeout expires. The timer starts when the text is displayed on + // the screen and stops when the TERMINAL RESPONSE is sent. The terminal + // shall pass the total display text duration (command execution duration) + // to the UICC using the TERMINAL RESPONSE. The time unit of the response + // is identical to the time unit of the requested variable timeout." + let duration = command && command.options && command.options.duration; + if (duration) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DURATION); + GsmPDUHelper.writeHexOctet(2); + GsmPDUHelper.writeHexOctet(duration.timeUnit); + GsmPDUHelper.writeHexOctet(duration.timeInterval); + } + } + + // Local Information + if (response.localInfo) { + let localInfo = response.localInfo; + + // Location Infomation + if (localInfo.locationInfo) { + ComprehensionTlvHelper.writeLocationInfoTlv(localInfo.locationInfo); + } + + // IMEI + if (localInfo.imei != null) { + let imei = localInfo.imei; + if (imei.length == 15) { + imei = imei + "0"; + } + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_IMEI); + GsmPDUHelper.writeHexOctet(8); + for (let i = 0; i < imei.length / 2; i++) { + GsmPDUHelper.writeHexOctet(parseInt(imei.substr(i * 2, 2), 16)); + } + } + + // Date and Time Zone + if (localInfo.date != null) { + ComprehensionTlvHelper.writeDateTimeZoneTlv(localInfo.date); + } + + // Language + if (localInfo.language) { + ComprehensionTlvHelper.writeLanguageTlv(localInfo.language); + } + } + + // Timer + if (response.timer) { + let timer = response.timer; + + if (timer.timerId) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(timer.timerId); + } + + if (timer.timerValue) { + ComprehensionTlvHelper.writeTimerValueTlv(timer.timerValue, false); + } + } + + // Calculate and write Parcel size to 1st mark + Buf.stopCalOutgoingSize(); + + Buf.writeInt32(0); + Buf.sendParcel(); + }, + + /** + * Send STK Envelope(Menu Selection) command. + * + * @param itemIdentifier + * @param helpRequested + */ + sendStkMenuSelection: function(command) { + command.tag = BER_MENU_SELECTION_TAG; + command.deviceId = { + sourceId :STK_DEVICE_ID_KEYPAD, + destinationId: STK_DEVICE_ID_SIM + }; + this.sendICCEnvelopeCommand(command); + }, + + /** + * Send STK Envelope(Timer Expiration) command. + * + * @param timer + */ + sendStkTimerExpiration: function(command) { + command.tag = BER_TIMER_EXPIRATION_TAG; + command.deviceId = { + sourceId: STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + command.timerId = command.timer.timerId; + command.timerValue = command.timer.timerValue; + this.sendICCEnvelopeCommand(command); + }, + + /** + * Send STK Envelope(Event Download) command. + * @param event + */ + sendStkEventDownload: function(command) { + command.tag = BER_EVENT_DOWNLOAD_TAG; + command.eventList = command.event.eventType; + switch (command.eventList) { + case STK_EVENT_TYPE_LOCATION_STATUS: + command.deviceId = { + sourceId :STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + command.locationStatus = command.event.locationStatus; + // Location info should only be provided when locationStatus is normal. + if (command.locationStatus == STK_SERVICE_STATE_NORMAL) { + command.locationInfo = command.event.locationInfo; + } + break; + case STK_EVENT_TYPE_MT_CALL: + command.deviceId = { + sourceId: STK_DEVICE_ID_NETWORK, + destinationId: STK_DEVICE_ID_SIM + }; + command.transactionId = 0; + command.address = command.event.number; + break; + case STK_EVENT_TYPE_CALL_DISCONNECTED: + command.cause = command.event.error; + // Fall through. + case STK_EVENT_TYPE_CALL_CONNECTED: + command.deviceId = { + sourceId: (command.event.isIssuedByRemote ? + STK_DEVICE_ID_NETWORK : STK_DEVICE_ID_ME), + destinationId: STK_DEVICE_ID_SIM + }; + command.transactionId = 0; + break; + case STK_EVENT_TYPE_USER_ACTIVITY: + command.deviceId = { + sourceId: STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + break; + case STK_EVENT_TYPE_IDLE_SCREEN_AVAILABLE: + command.deviceId = { + sourceId: STK_DEVICE_ID_DISPLAY, + destinationId: STK_DEVICE_ID_SIM + }; + break; + case STK_EVENT_TYPE_LANGUAGE_SELECTION: + command.deviceId = { + sourceId: STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + command.language = command.event.language; + break; + case STK_EVENT_TYPE_BROWSER_TERMINATION: + command.deviceId = { + sourceId: STK_DEVICE_ID_ME, + destinationId: STK_DEVICE_ID_SIM + }; + command.terminationCause = command.event.terminationCause; + break; + } + this.sendICCEnvelopeCommand(command); + }, + + /** + * Send REQUEST_STK_SEND_ENVELOPE_COMMAND to ICC. + * + * @param tag + * @patam deviceId + * @param [optioanl] itemIdentifier + * @param [optional] helpRequested + * @param [optional] eventList + * @param [optional] locationStatus + * @param [optional] locationInfo + * @param [optional] address + * @param [optional] transactionId + * @param [optional] cause + * @param [optional] timerId + * @param [optional] timerValue + * @param [optional] terminationCause + */ + sendICCEnvelopeCommand: function(options) { + if (DEBUG) { + this.context.debug("Stk Envelope " + JSON.stringify(options)); + } + + let Buf = this.context.Buf; + let ComprehensionTlvHelper = this.context.ComprehensionTlvHelper; + let GsmPDUHelper = this.context.GsmPDUHelper; + + Buf.newParcel(REQUEST_STK_SEND_ENVELOPE_COMMAND); + + // 1st mark for Parcel size + Buf.startCalOutgoingSize(function(size) { + // Parcel size is in string length, which costs 2 uint8 per char. + Buf.writeInt32(size / 2); + }); + + // Write a BER-TLV + GsmPDUHelper.writeHexOctet(options.tag); + // 2nd mark for BER length + Buf.startCalOutgoingSize(function(size) { + // BER length is in number of hexOctets, which costs 4 uint8 per hexOctet. + GsmPDUHelper.writeHexOctet(size / 4); + }); + + // Event List + if (options.eventList != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_EVENT_LIST | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.eventList); + } + + // Device Identifies + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DEVICE_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(2); + GsmPDUHelper.writeHexOctet(options.deviceId.sourceId); + GsmPDUHelper.writeHexOctet(options.deviceId.destinationId); + + // Item Identifier + if (options.itemIdentifier != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_ITEM_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.itemIdentifier); + } + + // Help Request + if (options.helpRequested) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_HELP_REQUEST | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(0); + // Help Request doesn't have value + } + + // Location Status + if (options.locationStatus != null) { + let len = options.locationStatus.length; + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_LOCATION_STATUS | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.locationStatus); + } + + // Location Info + if (options.locationInfo) { + ComprehensionTlvHelper.writeLocationInfoTlv(options.locationInfo); + } + + // Transaction Id + if (options.transactionId != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TRANSACTION_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.transactionId); + } + + // Address + if (options.address) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_ADDRESS | + COMPREHENSIONTLV_FLAG_CR); + let addressLength = options.address[0] == '+' ? options.address.length - 1 + : options.address.length; + ComprehensionTlvHelper.writeLength( + Math.ceil(addressLength / 2) + 1 // address BCD + TON + ); + this.context.ICCPDUHelper.writeDiallingNumber(options.address); + } + + // Cause of disconnection. + if (options.cause != null) { + ComprehensionTlvHelper.writeCauseTlv(options.cause); + } + + // Timer Identifier + if (options.timerId != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.timerId); + } + + // Timer Value + if (options.timerValue != null) { + ComprehensionTlvHelper.writeTimerValueTlv(options.timerValue, true); + } + + // Language + if (options.language) { + ComprehensionTlvHelper.writeLanguageTlv(options.language); + } + + // Browser Termination + if (options.terminationCause != null) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_BROWSER_TERMINATION_CAUSE | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(1); + GsmPDUHelper.writeHexOctet(options.terminationCause); + } + + // Calculate and write BER length to 2nd mark + Buf.stopCalOutgoingSize(); + + // Calculate and write Parcel size to 1st mark + Buf.stopCalOutgoingSize(); + + Buf.writeInt32(0); + Buf.sendParcel(); + }, + + /** + * Report STK Service is running. + */ + reportStkServiceIsRunning: function() { + this.context.Buf.simpleRequest(REQUEST_REPORT_STK_SERVICE_IS_RUNNING); + }, + + /** + * Process ICC status. + */ + _processICCStatus: function(iccStatus) { + // If |_waitingRadioTech| is true, we should not get app information because + // the |_isCdma| flag is not ready yet. Otherwise we may use wrong index to + // get app information, especially for the case that icc card has both cdma + // and gsm subscription. + if (this._waitingRadioTech) { + return; + } + + // When |iccStatus.cardState| is not CARD_STATE_PRESENT, set cardState to + // undetected. + if (iccStatus.cardState !== CARD_STATE_PRESENT) { + if (this.cardState !== GECKO_CARDSTATE_UNDETECTED) { + this.operator = null; + // We should send |cardstatechange| before |iccinfochange|, otherwise we + // may lost cardstatechange event when icc card becomes undetected. + this.cardState = GECKO_CARDSTATE_UNDETECTED; + this.sendChromeMessage({rilMessageType: "cardstatechange", + cardState: this.cardState}); + + this.iccInfo = {iccType: null}; + this.context.ICCUtilsHelper.handleICCInfoChange(); + } + return; + } + + if (RILQUIRKS_SUBSCRIPTION_CONTROL) { + // All appIndex is -1 means the subscription is not activated yet. + // Note that we don't support "ims" for now, so we don't take it into + // account. + let neetToActivate = iccStatus.cdmaSubscriptionAppIndex === -1 && + iccStatus.gsmUmtsSubscriptionAppIndex === -1; + if (neetToActivate && + // Note: setUiccSubscription works abnormally when RADIO is OFF, + // which causes SMS function broken in Flame. + // See bug 1008557 for detailed info. + this.radioState === GECKO_RADIOSTATE_ENABLED) { + for (let i = 0; i < iccStatus.apps.length; i++) { + this.setUiccSubscription({appIndex: i, enabled: true}); + } + } + } + + let newCardState; + let index = this._isCdma ? iccStatus.cdmaSubscriptionAppIndex + : iccStatus.gsmUmtsSubscriptionAppIndex; + let app = iccStatus.apps[index]; + if (app) { + // fetchICCRecords will need to read aid, so read aid here. + this.aid = app.aid; + this.appType = app.app_type; + this.iccInfo.iccType = GECKO_CARD_TYPE[this.appType]; + + switch (app.app_state) { + case CARD_APPSTATE_ILLEGAL: + newCardState = GECKO_CARDSTATE_ILLEGAL; + break; + case CARD_APPSTATE_PIN: + newCardState = GECKO_CARDSTATE_PIN_REQUIRED; + break; + case CARD_APPSTATE_PUK: + newCardState = GECKO_CARDSTATE_PUK_REQUIRED; + break; + case CARD_APPSTATE_SUBSCRIPTION_PERSO: + newCardState = PERSONSUBSTATE[app.perso_substate]; + break; + case CARD_APPSTATE_READY: + newCardState = GECKO_CARDSTATE_READY; + break; + case CARD_APPSTATE_UNKNOWN: + case CARD_APPSTATE_DETECTED: + // Fall through. + default: + newCardState = GECKO_CARDSTATE_UNKNOWN; + } + + let pin1State = app.pin1_replaced ? iccStatus.universalPINState : + app.pin1; + if (pin1State === CARD_PINSTATE_ENABLED_PERM_BLOCKED) { + newCardState = GECKO_CARDSTATE_PERMANENT_BLOCKED; + } + } else { + // Having incorrect app information, set card state to unknown. + newCardState = GECKO_CARDSTATE_UNKNOWN; + } + + let ICCRecordHelper = this.context.ICCRecordHelper; + // Try to get iccId only when cardState left GECKO_CARDSTATE_UNDETECTED. + if (iccStatus.cardState === CARD_STATE_PRESENT && + (this.cardState === GECKO_CARDSTATE_UNINITIALIZED || + this.cardState === GECKO_CARDSTATE_UNDETECTED)) { + ICCRecordHelper.readICCID(); + } + + if (this.cardState == newCardState) { + return; + } + + // This was moved down from CARD_APPSTATE_READY + this.requestNetworkInfo(); + if (newCardState == GECKO_CARDSTATE_READY) { + // For type SIM, we need to check EF_phase first. + // Other types of ICC we can send Terminal_Profile immediately. + if (this.appType == CARD_APPTYPE_SIM) { + this.context.SimRecordHelper.readSimPhase(); + } else if (RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD) { + this.sendStkTerminalProfile(STK_SUPPORTED_TERMINAL_PROFILE); + } + + ICCRecordHelper.fetchICCRecords(); + } + + this.cardState = newCardState; + this.sendChromeMessage({rilMessageType: "cardstatechange", + cardState: this.cardState}); + }, + + /** + * Helper for processing responses of functions such as enterICC* and changeICC*. + */ + _processEnterAndChangeICCResponses: function(length, options) { + options.retryCount = length ? this.context.Buf.readInt32List()[0] : -1; + this.sendChromeMessage(options); + }, + + // We combine all of the NETWORK_INFO_MESSAGE_TYPES into one "networkinfochange" + // message to the RadioInterfaceLayer, so we can avoid sending multiple + // VoiceInfoChanged events for both operator / voice_data_registration + // + // State management here is a little tricky. We need to know both: + // 1. Whether or not a response was received for each of the + // NETWORK_INFO_MESSAGE_TYPES + // 2. The outbound message that corresponds with that response -- but this + // only happens when internal state changes (i.e. it isn't guaranteed) + // + // To collect this state, each message response function first calls + // _receivedNetworkInfo, to mark the response as received. When the + // final response is received, a call to _sendPendingNetworkInfo is placed + // on the next tick of the worker thread. + // + // Since the original call to _receivedNetworkInfo happens at the top + // of the response handler, this gives the final handler a chance to + // queue up it's "changed" message by calling _sendNetworkInfoMessage if/when + // the internal state has actually changed. + _sendNetworkInfoMessage: function(type, message) { + if (!this._processingNetworkInfo) { + // We only combine these messages in the case of the combined request + // in requestNetworkInfo() + this.sendChromeMessage(message); + return; + } + + if (DEBUG) { + this.context.debug("Queuing " + type + " network info message: " + + JSON.stringify(message)); + } + this._pendingNetworkInfo[type] = message; + }, + + _receivedNetworkInfo: function(type) { + if (DEBUG) this.context.debug("Received " + type + " network info."); + if (!this._processingNetworkInfo) { + return; + } + + let pending = this._pendingNetworkInfo; + + // We still need to track states for events that aren't fired. + if (!(type in pending)) { + pending[type] = this.pendingNetworkType; + } + + // Pending network info is ready to be sent when no more messages + // are waiting for responses, but the combined payload hasn't been sent. + for (let i = 0; i < NETWORK_INFO_MESSAGE_TYPES.length; i++) { + let msgType = NETWORK_INFO_MESSAGE_TYPES[i]; + if (!(msgType in pending)) { + if (DEBUG) { + this.context.debug("Still missing some more network info, not " + + "notifying main thread."); + } + return; + } + } + + // Do a pass to clean up the processed messages that didn't create + // a response message, so we don't have unused keys in the outbound + // networkinfochanged message. + for (let key in pending) { + if (pending[key] == this.pendingNetworkType) { + delete pending[key]; + } + } + + if (DEBUG) { + this.context.debug("All pending network info has been received: " + + JSON.stringify(pending)); + } + + // Send the message on the next tick of the worker's loop, so we give the + // last message a chance to call _sendNetworkInfoMessage first. + setTimeout(this._sendPendingNetworkInfo.bind(this), 0); + }, + + _sendPendingNetworkInfo: function() { + this.sendChromeMessage(this._pendingNetworkInfo); + + this._processingNetworkInfo = false; + for (let i = 0; i < NETWORK_INFO_MESSAGE_TYPES.length; i++) { + delete this._pendingNetworkInfo[NETWORK_INFO_MESSAGE_TYPES[i]]; + } + + if (this._needRepollNetworkInfo) { + this._needRepollNetworkInfo = false; + this.requestNetworkInfo(); + } + }, + + /** + * Normalize the signal strength in dBm to the signal level from 0 to 100. + * + * @param signal + * The signal strength in dBm to normalize. + * @param min + * The signal strength in dBm maps to level 0. + * @param max + * The signal strength in dBm maps to level 100. + * + * @return level + * The signal level from 0 to 100. + */ + _processSignalLevel: function(signal, min, max) { + if (signal <= min) { + return 0; + } + + if (signal >= max) { + return 100; + } + + return Math.floor((signal - min) * 100 / (max - min)); + }, + + /** + * Process LTE signal strength to the signal info object. + * + * @param signal + * The signal object reported from RIL/modem. + * + * @return The object of signal strength info. + * Or null if invalid signal input. + * + * TODO: Bug 982013: reconsider the format of signal strength APIs for + * GSM/CDMA/LTE to expose details, such as rsrp and rsnnr, + * individually. + */ + _processLteSignal: function(signal) { + let info = { + voice: { + signalStrength: null, + relSignalStrength: null + }, + data: { + signalStrength: null, + relSignalStrength: null + } + }; + + // Referring to AOSP, use lteRSRP for signalStrength in dBm. + let signalStrength = (signal.lteRSRP === undefined || signal.lteRSRP === 0x7FFFFFFF) ? + null : signal.lteRSRP; + info.voice.signalStrength = info.data.signalStrength = signalStrength; + + // Referring to AOSP, first determine signalLevel based on RSRP and RSSNR, + // then on lteSignalStrength if RSRP and RSSNR are invalid. + let rsrpLevel = -1; + let rssnrLevel = -1; + if (signal.lteRSRP !== undefined && + signal.lteRSRP !== 0x7FFFFFFF && + signal.lteRSRP >= 44 && + signal.lteRSRP <= 140) { + rsrpLevel = this._processSignalLevel(signal.lteRSRP * -1, -115, -85); + } + + if (signal.lteRSSNR !== undefined && + signal.lteRSSNR !== 0x7FFFFFFF && + signal.lteRSSNR >= -200 && + signal.lteRSSNR <= 300) { + rssnrLevel = this._processSignalLevel(signal.lteRSSNR, -30, 130); + } + + if (rsrpLevel !== -1 && rssnrLevel !== -1) { + info.voice.relSignalStrength = info.data.relSignalStrength = + Math.min(rsrpLevel, rssnrLevel); + return info; + } + + let level = Math.max(rsrpLevel, rssnrLevel); + if (level !== -1) { + info.voice.relSignalStrength = info.data.relSignalStrength = level; + return info; + } + + // Valid values are 0-63 as defined in TS 27.007 clause 8.69. + if (signal.lteSignalStrength !== undefined && + signal.lteSignalStrength >= 0 && + signal.lteSignalStrength <= 63) { + level = this._processSignalLevel(signal.lteSignalStrength, 0, 12); + info.voice.relSignalStrength = info.data.relSignalStrength = level; + return info; + } + + return null; + }, + + _processSignalStrength: function(signal) { + let info = { + voice: { + signalStrength: null, + relSignalStrength: null + }, + data: { + signalStrength: null, + relSignalStrength: null + } + }; + + // During startup, |radioTech| is not yet defined, so we need to + // check it separately. + if (("radioTech" in this.voiceRegistrationState) && + !this._isGsmTechGroup(this.voiceRegistrationState.radioTech)) { + // CDMA RSSI. + // Valid values are positive integers. This value is the actual RSSI value + // multiplied by -1. Example: If the actual RSSI is -75, then this + // response value will be 75. + if (signal.cdmaDBM && signal.cdmaDBM > 0) { + let signalStrength = -1 * signal.cdmaDBM; + info.voice.signalStrength = signalStrength; + + // -105 and -70 are referred to AOSP's implementation. These values are + // not constants and can be customized based on different requirement. + let signalLevel = this._processSignalLevel(signalStrength, -105, -70); + info.voice.relSignalStrength = signalLevel; + } + + // EVDO RSSI. + // Valid values are positive integers. This value is the actual RSSI value + // multiplied by -1. Example: If the actual RSSI is -75, then this + // response value will be 75. + if (signal.evdoDBM && signal.evdoDBM > 0) { + let signalStrength = -1 * signal.evdoDBM; + info.data.signalStrength = signalStrength; + + // -105 and -70 are referred to AOSP's implementation. These values are + // not constants and can be customized based on different requirement. + let signalLevel = this._processSignalLevel(signalStrength, -105, -70); + info.data.relSignalStrength = signalLevel; + } + } else { + // Check LTE level first, and check GSM/UMTS level next if LTE one is not + // valid. + let lteInfo = this._processLteSignal(signal); + if (lteInfo) { + info = lteInfo; + } else { + // GSM signal strength. + // Valid values are 0-31 as defined in TS 27.007 8.5. + // 0 : -113 dBm or less + // 1 : -111 dBm + // 2...30: -109...-53 dBm + // 31 : -51 dBm + if (signal.gsmSignalStrength && + signal.gsmSignalStrength >= 0 && + signal.gsmSignalStrength <= 31) { + let signalStrength = -113 + 2 * signal.gsmSignalStrength; + info.voice.signalStrength = info.data.signalStrength = signalStrength; + + // -115 and -85 are referred to AOSP's implementation. These values are + // not constants and can be customized based on different requirement. + let signalLevel = this._processSignalLevel(signalStrength, -110, -85); + info.voice.relSignalStrength = info.data.relSignalStrength = signalLevel; + } + } + } + + info.rilMessageType = "signalstrengthchange"; + this._sendNetworkInfoMessage(NETWORK_INFO_SIGNAL, info); + }, + + /** + * Process the network registration flags. + * + * @return true if the state changed, false otherwise. + */ + _processCREG: function(curState, newState) { + let changed = false; + + let regState = this.parseInt(newState[0], NETWORK_CREG_STATE_UNKNOWN); + if (curState.regState === undefined || curState.regState !== regState) { + changed = true; + curState.regState = regState; + + curState.state = NETWORK_CREG_TO_GECKO_MOBILE_CONNECTION_STATE[regState]; + curState.connected = regState == NETWORK_CREG_STATE_REGISTERED_HOME || + regState == NETWORK_CREG_STATE_REGISTERED_ROAMING; + curState.roaming = regState == NETWORK_CREG_STATE_REGISTERED_ROAMING; + curState.emergencyCallsOnly = !curState.connected; + } + + if (!curState.cell) { + curState.cell = {}; + } + + // From TS 23.003, 0000 and 0xfffe are indicated that no valid LAI exists + // in MS. So we still need to report the '0000' as well. + let lac = this.parseInt(newState[1], -1, 16); + if (curState.cell.gsmLocationAreaCode === undefined || + curState.cell.gsmLocationAreaCode !== lac) { + curState.cell.gsmLocationAreaCode = lac; + changed = true; + } + + let cid = this.parseInt(newState[2], -1, 16); + if (curState.cell.gsmCellId === undefined || + curState.cell.gsmCellId !== cid) { + curState.cell.gsmCellId = cid; + changed = true; + } + + let radioTech = (newState[3] === undefined ? + NETWORK_CREG_TECH_UNKNOWN : + this.parseInt(newState[3], NETWORK_CREG_TECH_UNKNOWN)); + if (curState.radioTech === undefined || curState.radioTech !== radioTech) { + changed = true; + curState.radioTech = radioTech; + curState.type = GECKO_RADIO_TECH[radioTech] || null; + } + return changed; + }, + + _processVoiceRegistrationState: function(state) { + let rs = this.voiceRegistrationState; + let stateChanged = this._processCREG(rs, state); + if (stateChanged && rs.connected) { + this.getSmscAddress(); + } + + let cell = rs.cell; + if (this._isCdma) { + // Some variables below are not used. Comment them instead of removing to + // keep the information about state[x]. + let cdmaBaseStationId = this.parseInt(state[4], -1); + let cdmaBaseStationLatitude = this.parseInt(state[5], -2147483648); + let cdmaBaseStationLongitude = this.parseInt(state[6], -2147483648); + // let cssIndicator = this.parseInt(state[7]); + let cdmaSystemId = this.parseInt(state[8], -1); + let cdmaNetworkId = this.parseInt(state[9], -1); + // let roamingIndicator = this.parseInt(state[10]); + // let systemIsInPRL = this.parseInt(state[11]); + // let defaultRoamingIndicator = this.parseInt(state[12]); + // let reasonForDenial = this.parseInt(state[13]); + + if (cell.cdmaBaseStationId !== cdmaBaseStationId || + cell.cdmaBaseStationLatitude !== cdmaBaseStationLatitude || + cell.cdmaBaseStationLongitude !== cdmaBaseStationLongitude || + cell.cdmaSystemId !== cdmaSystemId || + cell.cdmaNetworkId !== cdmaNetworkId) { + stateChanged = true; + cell.cdmaBaseStationId = cdmaBaseStationId; + cell.cdmaBaseStationLatitude = cdmaBaseStationLatitude; + cell.cdmaBaseStationLongitude = cdmaBaseStationLongitude; + cell.cdmaSystemId = cdmaSystemId; + cell.cdmaNetworkId = cdmaNetworkId; + } + } + + if (stateChanged) { + rs.rilMessageType = "voiceregistrationstatechange"; + this._sendNetworkInfoMessage(NETWORK_INFO_VOICE_REGISTRATION_STATE, rs); + } + }, + + _processDataRegistrationState: function(state) { + let rs = this.dataRegistrationState; + let stateChanged = this._processCREG(rs, state); + if (stateChanged) { + rs.rilMessageType = "dataregistrationstatechange"; + this._sendNetworkInfoMessage(NETWORK_INFO_DATA_REGISTRATION_STATE, rs); + } + }, + + _processOperator: function(operatorData) { + if (operatorData.length < 3) { + if (DEBUG) { + this.context.debug("Expected at least 3 strings for operator."); + } + } + + if (!this.operator) { + this.operator = { + rilMessageType: "operatorchange", + longName: null, + shortName: null + }; + } + + let [longName, shortName, networkTuple] = operatorData; + let thisTuple = (this.operator.mcc || "") + (this.operator.mnc || ""); + + if (this.operator.longName !== longName || + this.operator.shortName !== shortName || + thisTuple !== networkTuple) { + + this.operator.mcc = null; + this.operator.mnc = null; + + if (networkTuple) { + try { + this._processNetworkTuple(networkTuple, this.operator); + } catch (e) { + if (DEBUG) this.context.debug("Error processing operator tuple: " + e); + } + } else { + // According to ril.h, the operator fields will be NULL when the operator + // is not currently registered. We can avoid trying to parse the numeric + // tuple in that case. + if (DEBUG) { + this.context.debug("Operator is currently unregistered"); + } + } + + this.operator.longName = longName; + this.operator.shortName = shortName; + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + if (ICCUtilsHelper.updateDisplayCondition()) { + ICCUtilsHelper.handleICCInfoChange(); + } + + // NETWORK_INFO_OPERATOR message will be sent out by overrideICCNetworkName + // itself if operator name is overridden after checking, or we have to + // do it by ourself. + if (!this.overrideICCNetworkName()) { + this._sendNetworkInfoMessage(NETWORK_INFO_OPERATOR, this.operator); + } + } + }, + + _processSuppSvcNotification: function(info) { + if (DEBUG) { + this.context.debug("handle supp svc notification: " + JSON.stringify(info)); + } + + if (info.notificationType !== 1) { + // We haven't supported MO intermediate result code, i.e. + // info.notificationType === 0, which refers to code1 defined in 3GPP + // 27.007 7.17. We only support partial MT unsolicited result code, + // referring to code2, for now. + return; + } + + let notification = null; + + switch (info.code) { + case SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD: + case SUPP_SVC_NOTIFICATION_CODE2_RETRIEVED: + notification = GECKO_SUPP_SVC_NOTIFICATION_FROM_CODE2[info.code]; + break; + default: + // Notification type not supported. + return; + } + + let message = {rilMessageType: "suppSvcNotification", + number: info.number, // could be empty. + notification: notification}; + this.sendChromeMessage(message); + }, + + _cancelEmergencyCbModeTimeout: function() { + if (this._exitEmergencyCbModeTimeoutID) { + clearTimeout(this._exitEmergencyCbModeTimeoutID); + this._exitEmergencyCbModeTimeoutID = null; + } + }, + + _handleChangedEmergencyCbMode: function(active) { + this._isInEmergencyCbMode = active; + + // Clear the existed timeout event. + this._cancelEmergencyCbModeTimeout(); + + // Start a new timeout event when entering the mode. + if (active) { + this._exitEmergencyCbModeTimeoutID = setTimeout( + this.exitEmergencyCbMode.bind(this), EMERGENCY_CB_MODE_TIMEOUT_MS); + } + + let message = {rilMessageType: "emergencyCbModeChange", + active: active, + timeoutMs: EMERGENCY_CB_MODE_TIMEOUT_MS}; + this.sendChromeMessage(message); + }, + + _updateNetworkSelectionMode: function(mode) { + if (this.networkSelectionMode === mode) { + return; + } + + let options = { + rilMessageType: "networkselectionmodechange", + mode: mode + }; + this.networkSelectionMode = mode; + this._sendNetworkInfoMessage(NETWORK_INFO_NETWORK_SELECTION_MODE, options); + }, + + _processNetworks: function() { + let strings = this.context.Buf.readStringList(); + let networks = []; + + for (let i = 0; i < strings.length; + i += RILQUIRKS_AVAILABLE_NETWORKS_EXTRA_STRING ? 5 : 4) { + let network = { + longName: strings[i], + shortName: strings[i + 1], + mcc: null, + mnc: null, + state: null + }; + + let networkTuple = strings[i + 2]; + try { + this._processNetworkTuple(networkTuple, network); + } catch (e) { + if (DEBUG) this.context.debug("Error processing operator tuple: " + e); + } + + let state = strings[i + 3]; + network.state = RIL_QAN_STATE_TO_GECKO_STATE[state]; + + networks.push(network); + } + return networks; + }, + + /** + * The "numeric" portion of the operator info is a tuple + * containing MCC (country code) and MNC (network code). + * AFAICT, MCC should always be 3 digits, making the remaining + * portion the MNC. + */ + _processNetworkTuple: function(networkTuple, network) { + let tupleLen = networkTuple.length; + + if (tupleLen == 5 || tupleLen == 6) { + network.mcc = networkTuple.substr(0, 3); + network.mnc = networkTuple.substr(3); + } else { + network.mcc = null; + network.mnc = null; + + throw new Error("Invalid network tuple (should be 5 or 6 digits): " + networkTuple); + } + }, + + /** + * Check if GSM radio access technology group. + */ + _isGsmTechGroup: function(radioTech) { + if (!radioTech) { + return true; + } + + switch(radioTech) { + case NETWORK_CREG_TECH_GPRS: + case NETWORK_CREG_TECH_EDGE: + case NETWORK_CREG_TECH_UMTS: + case NETWORK_CREG_TECH_HSDPA: + case NETWORK_CREG_TECH_HSUPA: + case NETWORK_CREG_TECH_HSPA: + case NETWORK_CREG_TECH_LTE: + case NETWORK_CREG_TECH_HSPAP: + case NETWORK_CREG_TECH_GSM: + case NETWORK_CREG_TECH_DCHSPAP_1: + case NETWORK_CREG_TECH_DCHSPAP_2: + return true; + } + + return false; + }, + + /** + * Process radio technology change. + */ + _processRadioTech: function(radioTech) { + let isCdma = !this._isGsmTechGroup(radioTech); + this.radioTech = radioTech; + + if (DEBUG) { + this.context.debug("Radio tech is set to: " + GECKO_RADIO_TECH[radioTech] + + ", it is a " + (isCdma?"cdma":"gsm") + " technology"); + } + + // We should request SIM information when + // 1. Radio state has been changed, so we are waiting for radioTech or + // 2. isCdma is different from this._isCdma. + if (this._waitingRadioTech || isCdma != this._isCdma) { + this._isCdma = isCdma; + this._waitingRadioTech = false; + this.getICCStatus(); + } + }, + + /** + * Helper for returning the TOA for the given dial string. + */ + _toaFromString: function(number) { + let toa = TOA_UNKNOWN; + if (number && number.length > 0 && number[0] == '+') { + toa = TOA_INTERNATIONAL; + } + return toa; + }, + + /** + * @param message A decoded SMS-DELIVER message. + * + * @see 3GPP TS 31.111 section 7.1.1 + */ + dataDownloadViaSMSPP: function(message) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let options = { + pid: message.pid, + dcs: message.dcs, + encoding: message.encoding, + }; + Buf.newParcel(REQUEST_STK_SEND_ENVELOPE_WITH_STATUS, options); + + Buf.seekIncoming(-1 * (Buf.getCurrentParcelSize() - Buf.getReadAvailable() + - 2 * Buf.UINT32_SIZE)); // Skip response_type & request_type. + let messageStringLength = Buf.readInt32(); // In semi-octets + let smscLength = GsmPDUHelper.readHexOctet(); // In octets, inclusive of TOA + let tpduLength = (messageStringLength / 2) - (smscLength + 1); // In octets + + // Device identities: 4 bytes + // Address: 0 or (2 + smscLength) + // SMS TPDU: (2 or 3) + tpduLength + let berLen = 4 + + (smscLength ? (2 + smscLength) : 0) + + (tpduLength <= 127 ? 2 : 3) + tpduLength; // In octets + + let parcelLength = (berLen <= 127 ? 2 : 3) + berLen; // In octets + Buf.writeInt32(parcelLength * 2); // In semi-octets + + // Write a BER-TLV + GsmPDUHelper.writeHexOctet(BER_SMS_PP_DOWNLOAD_TAG); + if (berLen > 127) { + GsmPDUHelper.writeHexOctet(0x81); + } + GsmPDUHelper.writeHexOctet(berLen); + + // Device Identifies-TLV + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DEVICE_ID | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(0x02); + GsmPDUHelper.writeHexOctet(STK_DEVICE_ID_NETWORK); + GsmPDUHelper.writeHexOctet(STK_DEVICE_ID_SIM); + + // Address-TLV + if (smscLength) { + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_ADDRESS); + GsmPDUHelper.writeHexOctet(smscLength); + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * smscLength); + } + + // SMS TPDU-TLV + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_SMS_TPDU | + COMPREHENSIONTLV_FLAG_CR); + if (tpduLength > 127) { + GsmPDUHelper.writeHexOctet(0x81); + } + GsmPDUHelper.writeHexOctet(tpduLength); + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * tpduLength); + + // Write 2 string delimitors for the total string length must be even. + Buf.writeStringDelimiter(0); + + Buf.sendParcel(); + }, + + /** + * @param success A boolean value indicating the result of previous + * SMS-DELIVER message handling. + * @param responsePduLen ICC IO response PDU length in octets. + * @param options An object that contains four attributes: `pid`, `dcs`, + * `encoding` and `responsePduLen`. + * + * @see 3GPP TS 23.040 section 9.2.2.1a + */ + acknowledgeIncomingGsmSmsWithPDU: function(success, responsePduLen, options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + Buf.newParcel(REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU); + + // Two strings. + Buf.writeInt32(2); + + // String 1: Success + Buf.writeString(success ? "1" : "0"); + + // String 2: RP-ACK/RP-ERROR PDU + Buf.writeInt32(2 * (responsePduLen + (success ? 5 : 6))); // In semi-octet + // 1. TP-MTI & TP-UDHI + GsmPDUHelper.writeHexOctet(PDU_MTI_SMS_DELIVER); + if (!success) { + // 2. TP-FCS + GsmPDUHelper.writeHexOctet(PDU_FCS_USIM_DATA_DOWNLOAD_ERROR); + } + // 3. TP-PI + GsmPDUHelper.writeHexOctet(PDU_PI_USER_DATA_LENGTH | + PDU_PI_DATA_CODING_SCHEME | + PDU_PI_PROTOCOL_IDENTIFIER); + // 4. TP-PID + GsmPDUHelper.writeHexOctet(options.pid); + // 5. TP-DCS + GsmPDUHelper.writeHexOctet(options.dcs); + // 6. TP-UDL + if (options.encoding == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + GsmPDUHelper.writeHexOctet(Math.floor(responsePduLen * 8 / 7)); + } else { + GsmPDUHelper.writeHexOctet(responsePduLen); + } + // TP-UD + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * responsePduLen); + // Write 2 string delimitors for the total string length must be even. + Buf.writeStringDelimiter(0); + + Buf.sendParcel(); + }, + + /** + * @param message A decoded SMS-DELIVER message. + */ + writeSmsToSIM: function(message) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + Buf.newParcel(REQUEST_WRITE_SMS_TO_SIM); + + // Write EFsms Status + Buf.writeInt32(EFSMS_STATUS_FREE); + + Buf.seekIncoming(-1 * (Buf.getCurrentParcelSize() - Buf.getReadAvailable() + - 2 * Buf.UINT32_SIZE)); // Skip response_type & request_type. + let messageStringLength = Buf.readInt32(); // In semi-octets + let smscLength = GsmPDUHelper.readHexOctet(); // In octets, inclusive of TOA + let pduLength = (messageStringLength / 2) - (smscLength + 1); // In octets + + // 1. Write PDU first. + if (smscLength > 0) { + Buf.seekIncoming(smscLength * Buf.PDU_HEX_OCTET_SIZE); + } + // Write EFsms PDU string length + Buf.writeInt32(2 * pduLength); // In semi-octets + if (pduLength) { + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * pduLength); + } + // Write 2 string delimitors for the total string length must be even. + Buf.writeStringDelimiter(0); + + // 2. Write SMSC + // Write EFsms SMSC string length + Buf.writeInt32(2 * (smscLength + 1)); // Plus smscLength itself, in semi-octets + // Write smscLength + GsmPDUHelper.writeHexOctet(smscLength); + // Write TOA & SMSC Address + if (smscLength) { + Buf.seekIncoming(-1 * (Buf.getCurrentParcelSize() - Buf.getReadAvailable() + - 2 * Buf.UINT32_SIZE // Skip response_type, request_type. + - 2 * Buf.PDU_HEX_OCTET_SIZE)); // Skip messageStringLength & smscLength. + Buf.copyIncomingToOutgoing(Buf.PDU_HEX_OCTET_SIZE * smscLength); + } + // Write 2 string delimitors for the total string length must be even. + Buf.writeStringDelimiter(0); + + Buf.sendParcel(); + }, + + /** + * Helper to delegate the received sms segment to RadioInterface to process. + * + * @param message + * Received sms message. + * + * @return MOZ_FCS_WAIT_FOR_EXPLICIT_ACK + */ + _processSmsMultipart: function(message) { + message.rilMessageType = "sms-received"; + + this.sendChromeMessage(message); + + return MOZ_FCS_WAIT_FOR_EXPLICIT_ACK; + }, + + /** + * Helper for processing SMS-STATUS-REPORT PDUs. + * + * @param length + * Length of SMS string in the incoming parcel. + * + * @return A failure cause defined in 3GPP 23.040 clause 9.2.3.22. + */ + _processSmsStatusReport: function(length) { + let [message, result] = this.context.GsmPDUHelper.processReceivedSms(length); + if (!message) { + if (DEBUG) this.context.debug("invalid SMS-STATUS-REPORT"); + return PDU_FCS_UNSPECIFIED; + } + + let options = this._pendingSentSmsMap[message.messageRef]; + if (!options) { + if (DEBUG) this.context.debug("no pending SMS-SUBMIT message"); + return PDU_FCS_OK; + } + + let status = message.status; + + // 3GPP TS 23.040 9.2.3.15 `The MS shall interpret any reserved values as + // "Service Rejected"(01100011) but shall store them exactly as received.` + if ((status >= 0x80) + || ((status >= PDU_ST_0_RESERVED_BEGIN) + && (status < PDU_ST_0_SC_SPECIFIC_BEGIN)) + || ((status >= PDU_ST_1_RESERVED_BEGIN) + && (status < PDU_ST_1_SC_SPECIFIC_BEGIN)) + || ((status >= PDU_ST_2_RESERVED_BEGIN) + && (status < PDU_ST_2_SC_SPECIFIC_BEGIN)) + || ((status >= PDU_ST_3_RESERVED_BEGIN) + && (status < PDU_ST_3_SC_SPECIFIC_BEGIN)) + ) { + status = PDU_ST_3_SERVICE_REJECTED; + } + + // Pending. Waiting for next status report. + if ((status >>> 5) == 0x01) { + if (DEBUG) this.context.debug("SMS-STATUS-REPORT: delivery still pending"); + return PDU_FCS_OK; + } + + delete this._pendingSentSmsMap[message.messageRef]; + + let deliveryStatus = ((status >>> 5) === 0x00) + ? GECKO_SMS_DELIVERY_STATUS_SUCCESS + : GECKO_SMS_DELIVERY_STATUS_ERROR; + this.sendChromeMessage({ + rilMessageType: options.rilMessageType, + rilMessageToken: options.rilMessageToken, + deliveryStatus: deliveryStatus + }); + + return PDU_FCS_OK; + }, + + /** + * Helper for processing CDMA SMS Delivery Acknowledgment Message + * + * @param message + * decoded SMS Delivery ACK message from CdmaPDUHelper. + * + * @return A failure cause defined in 3GPP 23.040 clause 9.2.3.22. + */ + _processCdmaSmsStatusReport: function(message) { + let options = this._pendingSentSmsMap[message.msgId]; + if (!options) { + if (DEBUG) this.context.debug("no pending SMS-SUBMIT message"); + return PDU_FCS_OK; + } + + if (message.errorClass === 2) { + if (DEBUG) { + this.context.debug("SMS-STATUS-REPORT: delivery still pending, " + + "msgStatus: " + message.msgStatus); + } + return PDU_FCS_OK; + } + + delete this._pendingSentSmsMap[message.msgId]; + + if (message.errorClass === -1 && message.body) { + // Process as normal incoming SMS, if errorClass is invalid + // but message body is available. + return this._processSmsMultipart(message); + } + + let deliveryStatus = (message.errorClass === 0) + ? GECKO_SMS_DELIVERY_STATUS_SUCCESS + : GECKO_SMS_DELIVERY_STATUS_ERROR; + this.sendChromeMessage({ + rilMessageType: options.rilMessageType, + rilMessageToken: options.rilMessageToken, + deliveryStatus: deliveryStatus + }); + + return PDU_FCS_OK; + }, + + /** + * Helper for processing CDMA SMS WAP Push Message + * + * @param message + * decoded WAP message from CdmaPDUHelper. + * + * @return A failure cause defined in 3GPP 23.040 clause 9.2.3.22. + */ + _processCdmaSmsWapPush: function(message) { + if (!message.data) { + if (DEBUG) this.context.debug("no data inside WAP Push message."); + return PDU_FCS_OK; + } + + // See 6.5. MAPPING OF WDP TO CDMA SMS in WAP-295-WDP. + // + // Field | Length (bits) + // ----------------------------------------- + // MSG_TYPE | 8 + // TOTAL_SEGMENTS | 8 + // SEGMENT_NUMBER | 8 + // DATAGRAM | (NUM_FIELDS - 3) * 8 + let index = 0; + if (message.data[index++] !== 0) { + if (DEBUG) this.context.debug("Ignore a WAP Message which is not WDP."); + return PDU_FCS_OK; + } + + // 1. Originator Address in SMS-TL + Message_Id in SMS-TS are used to identify a unique WDP datagram. + // 2. TOTAL_SEGMENTS, SEGMENT_NUMBER are used to verify that a complete + // datagram has been received and is ready to be passed to a higher layer. + message.header = { + segmentRef: message.msgId, + segmentMaxSeq: message.data[index++], + segmentSeq: message.data[index++] + 1 // It's zero-based in CDMA WAP Push. + }; + + if (message.header.segmentSeq > message.header.segmentMaxSeq) { + if (DEBUG) this.context.debug("Wrong WDP segment info."); + return PDU_FCS_OK; + } + + // Ports are only specified in 1st segment. + if (message.header.segmentSeq == 1) { + message.header.originatorPort = message.data[index++] << 8; + message.header.originatorPort |= message.data[index++]; + message.header.destinationPort = message.data[index++] << 8; + message.header.destinationPort |= message.data[index++]; + } + + message.data = message.data.subarray(index); + + return this._processSmsMultipart(message); + }, + + /** + * Helper for processing sent multipart SMS. + */ + _processSentSmsSegment: function(options) { + // Setup attributes for sending next segment + let next = options.segmentSeq; + options.body = options.segments[next].body; + options.encodedBodyLength = options.segments[next].encodedBodyLength; + options.segmentSeq = next + 1; + + this.sendSMS(options); + }, + + /** + * Helper for processing result of send SMS. + * + * @param length + * Length of SMS string in the incoming parcel. + * @param options + * Sms information. + */ + _processSmsSendResult: function(length, options) { + if (options.errorMsg) { + if (DEBUG) { + this.context.debug("_processSmsSendResult: errorMsg = " + + options.errorMsg); + } + + this.sendChromeMessage({ + rilMessageType: options.rilMessageType, + rilMessageToken: options.rilMessageToken, + errorMsg: options.errorMsg, + }); + return; + } + + let Buf = this.context.Buf; + options.messageRef = Buf.readInt32(); + options.ackPDU = Buf.readString(); + options.errorCode = Buf.readInt32(); + + if ((options.segmentMaxSeq > 1) + && (options.segmentSeq < options.segmentMaxSeq)) { + // Not last segment + this._processSentSmsSegment(options); + } else { + // Last segment sent with success. + if (options.requestStatusReport) { + if (DEBUG) { + this.context.debug("waiting SMS-STATUS-REPORT for messageRef " + + options.messageRef); + } + this._pendingSentSmsMap[options.messageRef] = options; + } + + this.sendChromeMessage({ + rilMessageType: options.rilMessageType, + rilMessageToken: options.rilMessageToken, + }); + } + }, + + _processReceivedSmsCbPage: function(original) { + if (original.numPages <= 1) { + if (original.body) { + original.fullBody = original.body; + delete original.body; + } else if (original.data) { + original.fullData = original.data; + delete original.data; + } + return original; + } + + // Hash = <serial>:<mcc>:<mnc>:<lac>:<cid> + let hash = original.serial + ":" + this.iccInfo.mcc + ":" + + this.iccInfo.mnc + ":"; + switch (original.geographicalScope) { + case CB_GSM_GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: + case CB_GSM_GEOGRAPHICAL_SCOPE_CELL_WIDE: + hash += this.voiceRegistrationState.cell.gsmLocationAreaCode + ":" + + this.voiceRegistrationState.cell.gsmCellId; + break; + case CB_GSM_GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE: + hash += this.voiceRegistrationState.cell.gsmLocationAreaCode + ":"; + break; + default: + hash += ":"; + break; + } + + let index = original.pageIndex; + + let options = this._receivedSmsCbPagesMap[hash]; + if (!options) { + options = original; + this._receivedSmsCbPagesMap[hash] = options; + + options.receivedPages = 0; + options.pages = []; + } else if (options.pages[index]) { + // Duplicated page? + if (DEBUG) { + this.context.debug("Got duplicated page no." + index + + " of a multipage SMSCB: " + JSON.stringify(original)); + } + return null; + } + + if (options.encoding == PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + options.pages[index] = original.data; + delete original.data; + } else { + options.pages[index] = original.body; + delete original.body; + } + options.receivedPages++; + if (options.receivedPages < options.numPages) { + if (DEBUG) { + this.context.debug("Got page no." + index + " of a multipage SMSCB: " + + JSON.stringify(options)); + } + return null; + } + + // Remove from map + delete this._receivedSmsCbPagesMap[hash]; + + // Rebuild full body + if (options.encoding == PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + // Uint8Array doesn't have `concat`, so we have to merge all pages by hand. + let fullDataLen = 0; + for (let i = 1; i <= options.numPages; i++) { + fullDataLen += options.pages[i].length; + } + + options.fullData = new Uint8Array(fullDataLen); + for (let d= 0, i = 1; i <= options.numPages; i++) { + let data = options.pages[i]; + for (let j = 0; j < data.length; j++) { + options.fullData[d++] = data[j]; + } + } + } else { + options.fullBody = options.pages.join(""); + } + + if (DEBUG) { + this.context.debug("Got full multipage SMSCB: " + JSON.stringify(options)); + } + + return options; + }, + + _mergeCellBroadcastConfigs: function(list, from, to) { + if (!list) { + return [from, to]; + } + + for (let i = 0, f1, t1; i < list.length;) { + f1 = list[i++]; + t1 = list[i++]; + if (to == f1) { + // ...[from]...[to|f1]...(t1) + list[i - 2] = from; + return list; + } + + if (to < f1) { + // ...[from]...(to)...[f1] or ...[from]...(to)[f1] + if (i > 2) { + // Not the first range pair, merge three arrays. + return list.slice(0, i - 2).concat([from, to]).concat(list.slice(i - 2)); + } else { + return [from, to].concat(list); + } + } + + if (from > t1) { + // ...[f1]...(t1)[from] or ...[f1]...(t1)...[from] + continue; + } + + // Have overlap or merge-able adjacency with [f1]...(t1). Replace it + // with [min(from, f1)]...(max(to, t1)). + + let changed = false; + if (from < f1) { + // [from]...[f1]...(t1) or [from][f1]...(t1) + // Save minimum from value. + list[i - 2] = from; + changed = true; + } + + if (to <= t1) { + // [from]...[to](t1) or [from]...(to|t1) + // Can't have further merge-able adjacency. Return. + return list; + } + + // Try merging possible next adjacent range. + let j = i; + for (let f2, t2; j < list.length;) { + f2 = list[j++]; + t2 = list[j++]; + if (to > t2) { + // [from]...[f2]...[t2]...(to) or [from]...[f2]...[t2](to) + // Merge next adjacent range again. + continue; + } + + if (to < t2) { + if (to < f2) { + // [from]...(to)[f2] or [from]...(to)...[f2] + // Roll back and give up. + j -= 2; + } else if (to < t2) { + // [from]...[to|f2]...(t2), or [from]...[f2]...[to](t2) + // Merge to [from]...(t2) and give up. + to = t2; + } + } + + break; + } + + // Save maximum to value. + list[i - 1] = to; + + if (j != i) { + // Remove merged adjacent ranges. + let ret = list.slice(0, i); + if (j < list.length) { + ret = ret.concat(list.slice(j)); + } + return ret; + } + + return list; + } + + // Append to the end. + list.push(from); + list.push(to); + + return list; + }, + + _isCellBroadcastConfigReady: function() { + if (!("MMI" in this.cellBroadcastConfigs)) { + return false; + } + + // CBMI should be ready in GSM. + if (!this._isCdma && + (!("CBMI" in this.cellBroadcastConfigs) || + !("CBMID" in this.cellBroadcastConfigs) || + !("CBMIR" in this.cellBroadcastConfigs))) { + return false; + } + + return true; + }, + + /** + * Merge all members of cellBroadcastConfigs into mergedCellBroadcastConfig. + */ + _mergeAllCellBroadcastConfigs: function() { + if (!this._isCellBroadcastConfigReady()) { + if (DEBUG) { + this.context.debug("cell broadcast configs not ready, waiting ..."); + } + return; + } + + // Prepare cell broadcast config. CBMI* are only used in GSM. + let usedCellBroadcastConfigs = {MMI: this.cellBroadcastConfigs.MMI}; + if (!this._isCdma) { + usedCellBroadcastConfigs.CBMI = this.cellBroadcastConfigs.CBMI; + usedCellBroadcastConfigs.CBMID = this.cellBroadcastConfigs.CBMID; + usedCellBroadcastConfigs.CBMIR = this.cellBroadcastConfigs.CBMIR; + } + + if (DEBUG) { + this.context.debug("Cell Broadcast search lists: " + + JSON.stringify(usedCellBroadcastConfigs)); + } + + let list = null; + for (let key in usedCellBroadcastConfigs) { + let ll = usedCellBroadcastConfigs[key]; + if (ll == null) { + continue; + } + + for (let i = 0; i < ll.length; i += 2) { + list = this._mergeCellBroadcastConfigs(list, ll[i], ll[i + 1]); + } + } + + if (DEBUG) { + this.context.debug("Cell Broadcast search lists(merged): " + + JSON.stringify(list)); + } + this.mergedCellBroadcastConfig = list; + this.updateCellBroadcastConfig(); + }, + + /** + * Check whether search list from settings is settable by MMI, that is, + * whether the range is bounded in any entries of CB_NON_MMI_SETTABLE_RANGES. + */ + _checkCellBroadcastMMISettable: function(from, to) { + if ((to <= from) || (from >= 65536) || (from < 0)) { + return false; + } + + if (!this._isCdma) { + // GSM not settable ranges. + for (let i = 0, f, t; i < CB_NON_MMI_SETTABLE_RANGES.length;) { + f = CB_NON_MMI_SETTABLE_RANGES[i++]; + t = CB_NON_MMI_SETTABLE_RANGES[i++]; + if ((from < t) && (to > f)) { + // Have overlap. + return false; + } + } + } + + return true; + }, + + /** + * Convert Cell Broadcast settings string into search list. + */ + _convertCellBroadcastSearchList: function(searchListStr) { + let parts = searchListStr && searchListStr.split(","); + if (!parts) { + return null; + } + + let list = null; + let result, from, to; + for (let range of parts) { + // Match "12" or "12-34". The result will be ["12", "12", null] or + // ["12-34", "12", "34"]. + result = range.match(/^(\d+)(?:-(\d+))?$/); + if (!result) { + throw "Invalid format"; + } + + from = parseInt(result[1], 10); + to = (result[2]) ? parseInt(result[2], 10) + 1 : from + 1; + if (!this._checkCellBroadcastMMISettable(from, to)) { + throw "Invalid range"; + } + + if (list == null) { + list = []; + } + list.push(from); + list.push(to); + } + + return list; + }, + + /** + * Handle incoming messages from the main UI thread. + * + * @param message + * Object containing the message. Messages are supposed + */ + handleChromeMessage: function(message) { + if (DEBUG) { + this.context.debug("Received chrome message " + JSON.stringify(message)); + } + let method = this[message.rilMessageType]; + if (typeof method != "function") { + if (DEBUG) { + this.context.debug("Don't know what to do with message " + + JSON.stringify(message)); + } + return; + } + method.call(this, message); + }, + + /** + * Process STK Proactive Command. + */ + processStkProactiveCommand: function() { + let Buf = this.context.Buf; + let length = Buf.readInt32(); + let berTlv; + try { + berTlv = this.context.BerTlvHelper.decode(length / 2); + } catch (e) { + if (DEBUG) this.context.debug("processStkProactiveCommand : " + e); + this.sendStkTerminalResponse({ + resultCode: STK_RESULT_CMD_DATA_NOT_UNDERSTOOD}); + return; + } + + Buf.readStringDelimiter(length); + + let ctlvs = berTlv.value; + let ctlv = this.context.StkProactiveCmdHelper.searchForTag( + COMPREHENSIONTLV_TAG_COMMAND_DETAILS, ctlvs); + if (!ctlv) { + this.sendStkTerminalResponse({ + resultCode: STK_RESULT_CMD_DATA_NOT_UNDERSTOOD}); + throw new Error("Can't find COMMAND_DETAILS ComprehensionTlv"); + } + + let cmdDetails = ctlv.value; + if (DEBUG) { + this.context.debug("commandNumber = " + cmdDetails.commandNumber + + " typeOfCommand = " + cmdDetails.typeOfCommand.toString(16) + + " commandQualifier = " + cmdDetails.commandQualifier); + } + + // STK_CMD_MORE_TIME need not to propagate event to chrome. + if (cmdDetails.typeOfCommand == STK_CMD_MORE_TIME) { + this.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_OK}); + return; + } + + this.context.StkCommandParamsFactory.createParam(cmdDetails, + ctlvs, + (aResult) => { + cmdDetails.options = aResult; + cmdDetails.rilMessageType = "stkcommand"; + this.sendChromeMessage(cmdDetails); + }); + }, + + sendDefaultResponse: function(options) { + if (!options.rilMessageType) { + return; + } + + this.sendChromeMessage(options); + }, + + /** + * Send messages to the main thread. + */ + sendChromeMessage: function(message) { + message.rilMessageClientId = this.context.clientId; + postMessage(message); + }, + + /** + * Handle incoming requests from the RIL. We find the method that + * corresponds to the request type. Incidentally, the request type + * _is_ the method name, so that's easy. + */ + + handleParcel: function(request_type, length, options) { + let method = this[request_type]; + if (typeof method == "function") { + if (DEBUG) this.context.debug("Handling parcel as " + method.name); + method.call(this, length, options); + } + + if (this.telephonyRequestQueue.isValidRequest(request_type)) { + this.telephonyRequestQueue.pop(request_type); + } + } +}; + +RilObject.prototype[REQUEST_GET_SIM_STATUS] = function REQUEST_GET_SIM_STATUS(length, options) { + if (options.errorMsg) { + return; + } + + let iccStatus = {}; + let Buf = this.context.Buf; + iccStatus.cardState = Buf.readInt32(); // CARD_STATE_* + iccStatus.universalPINState = Buf.readInt32(); // CARD_PINSTATE_* + iccStatus.gsmUmtsSubscriptionAppIndex = Buf.readInt32(); + iccStatus.cdmaSubscriptionAppIndex = Buf.readInt32(); + iccStatus.imsSubscriptionAppIndex = Buf.readInt32(); + + let apps_length = Buf.readInt32(); + if (apps_length > CARD_MAX_APPS) { + apps_length = CARD_MAX_APPS; + } + + iccStatus.apps = []; + for (let i = 0 ; i < apps_length ; i++) { + iccStatus.apps.push({ + app_type: Buf.readInt32(), // CARD_APPTYPE_* + app_state: Buf.readInt32(), // CARD_APPSTATE_* + perso_substate: Buf.readInt32(), // CARD_PERSOSUBSTATE_* + aid: Buf.readString(), + app_label: Buf.readString(), + pin1_replaced: Buf.readInt32(), + pin1: Buf.readInt32(), + pin2: Buf.readInt32() + }); + if (RILQUIRKS_SIM_APP_STATE_EXTRA_FIELDS) { + Buf.readInt32(); + Buf.readInt32(); + Buf.readInt32(); + Buf.readInt32(); + } + } + + if (DEBUG) this.context.debug("iccStatus: " + JSON.stringify(iccStatus)); + this._processICCStatus(iccStatus); +}; +RilObject.prototype[REQUEST_ENTER_SIM_PIN] = function REQUEST_ENTER_SIM_PIN(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_ENTER_SIM_PUK] = function REQUEST_ENTER_SIM_PUK(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_ENTER_SIM_PIN2] = function REQUEST_ENTER_SIM_PIN2(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_ENTER_SIM_PUK2] = function REQUEST_ENTER_SIM_PUK(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_CHANGE_SIM_PIN] = function REQUEST_CHANGE_SIM_PIN(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_CHANGE_SIM_PIN2] = function REQUEST_CHANGE_SIM_PIN2(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE] = + function REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE(length, options) { + this._processEnterAndChangeICCResponses(length, options); +}; +RilObject.prototype[REQUEST_GET_CURRENT_CALLS] = function REQUEST_GET_CURRENT_CALLS(length, options) { + // Retry getCurrentCalls several times when error occurs. + if (options.errorMsg) { + if (this._getCurrentCallsRetryCount < GET_CURRENT_CALLS_RETRY_MAX) { + this._getCurrentCallsRetryCount++; + this.getCurrentCalls(options); + } else { + this.sendDefaultResponse(options); + } + return; + } + + this._getCurrentCallsRetryCount = 0; + + let Buf = this.context.Buf; + let calls_length = 0; + // The RIL won't even send us the length integer if there are no active calls. + // So only read this integer if the parcel actually has it. + if (length) { + calls_length = Buf.readInt32(); + } + + let calls = {}; + for (let i = 0; i < calls_length; i++) { + let call = {}; + + // Extra uint32 field to get correct callIndex and rest of call data for + // call waiting feature. + if (RILQUIRKS_EXTRA_UINT32_2ND_CALL && i > 0) { + Buf.readInt32(); + } + + call.state = Buf.readInt32(); // CALL_STATE_* + call.callIndex = Buf.readInt32(); // GSM index (1-based) + call.toa = Buf.readInt32(); + call.isMpty = Boolean(Buf.readInt32()); + call.isMT = Boolean(Buf.readInt32()); + call.als = Buf.readInt32(); + call.isVoice = Boolean(Buf.readInt32()); + call.isVoicePrivacy = Boolean(Buf.readInt32()); + if (RILQUIRKS_CALLSTATE_EXTRA_UINT32) { + Buf.readInt32(); + } + call.number = Buf.readString(); + call.numberPresentation = Buf.readInt32(); // CALL_PRESENTATION_* + call.name = Buf.readString(); + call.namePresentation = Buf.readInt32(); + + call.uusInfo = null; + let uusInfoPresent = Buf.readInt32(); + if (uusInfoPresent == 1) { + call.uusInfo = { + type: Buf.readInt32(), + dcs: Buf.readInt32(), + userData: null //XXX TODO byte array?!? + }; + } + + if (call.isVoice) { + calls[call.callIndex] = call; + } + } + + options.calls = calls; + options.rilMessageType = options.rilMessageType || "currentCalls"; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_DIAL] = function REQUEST_DIAL(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_DIAL_EMERGENCY_CALL] = function REQUEST_DIAL_EMERGENCY_CALL(length, options) { + RilObject.prototype[REQUEST_DIAL].call(this, length, options); +}; +RilObject.prototype[REQUEST_GET_IMSI] = function REQUEST_GET_IMSI(length, options) { + if (options.errorMsg) { + return; + } + + this.iccInfoPrivate.imsi = this.context.Buf.readString(); + if (DEBUG) { + this.context.debug("IMSI: " + this.iccInfoPrivate.imsi); + } + + options.rilMessageType = "iccimsi"; + options.imsi = this.iccInfoPrivate.imsi; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_HANGUP] = function REQUEST_HANGUP(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_HANGUP_WAITING_OR_BACKGROUND] = function REQUEST_HANGUP_WAITING_OR_BACKGROUND(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND] = function REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE] = function REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_CONFERENCE] = function REQUEST_CONFERENCE(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_UDUB] = function REQUEST_UDUB(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_LAST_CALL_FAIL_CAUSE] = function REQUEST_LAST_CALL_FAIL_CAUSE(length, options) { + // Treat it as CALL_FAIL_ERROR_UNSPECIFIED if the request failed. + let failCause = CALL_FAIL_ERROR_UNSPECIFIED; + + if (!options.errorMsg) { + let Buf = this.context.Buf; + let num = length ? Buf.readInt32() : 0; + + if (num) { + let causeNum = Buf.readInt32(); + failCause = RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[causeNum] || failCause; + } + if (DEBUG) this.context.debug("Last call fail cause: " + failCause); + } + + options.failCause = failCause; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SIGNAL_STRENGTH] = function REQUEST_SIGNAL_STRENGTH(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_SIGNAL); + + if (options.errorMsg) { + return; + } + + let Buf = this.context.Buf; + let signal = {}; + + signal.gsmSignalStrength = Buf.readInt32(); + signal.gsmBitErrorRate = Buf.readInt32(); + if (RILQUIRKS_SIGNAL_EXTRA_INT32) { + Buf.readInt32(); + } + signal.cdmaDBM = Buf.readInt32(); + signal.cdmaECIO = Buf.readInt32(); + signal.evdoDBM = Buf.readInt32(); + signal.evdoECIO = Buf.readInt32(); + signal.evdoSNR = Buf.readInt32(); + + signal.lteSignalStrength = Buf.readInt32(); + signal.lteRSRP = Buf.readInt32(); + signal.lteRSRQ = Buf.readInt32(); + signal.lteRSSNR = Buf.readInt32(); + signal.lteCQI = Buf.readInt32(); + + if (DEBUG) this.context.debug("signal strength: " + JSON.stringify(signal)); + + this._processSignalStrength(signal); +}; +RilObject.prototype[REQUEST_VOICE_REGISTRATION_STATE] = function REQUEST_VOICE_REGISTRATION_STATE(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_VOICE_REGISTRATION_STATE); + + if (options.errorMsg) { + return; + } + + let state = this.context.Buf.readStringList(); + if (DEBUG) this.context.debug("voice registration state: " + state); + + this._processVoiceRegistrationState(state); +}; +RilObject.prototype[REQUEST_DATA_REGISTRATION_STATE] = function REQUEST_DATA_REGISTRATION_STATE(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_DATA_REGISTRATION_STATE); + + if (options.errorMsg) { + return; + } + + let state = this.context.Buf.readStringList(); + this._processDataRegistrationState(state); +}; +RilObject.prototype[REQUEST_OPERATOR] = function REQUEST_OPERATOR(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_OPERATOR); + + if (options.errorMsg) { + return; + } + + let operatorData = this.context.Buf.readStringList(); + if (DEBUG) this.context.debug("Operator: " + operatorData); + this._processOperator(operatorData); +}; +RilObject.prototype[REQUEST_RADIO_POWER] = function REQUEST_RADIO_POWER(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_DTMF] = null; +RilObject.prototype[REQUEST_SEND_SMS] = function REQUEST_SEND_SMS(length, options) { + this._processSmsSendResult(length, options); +}; +RilObject.prototype[REQUEST_SEND_SMS_EXPECT_MORE] = null; + +RilObject.prototype[REQUEST_SETUP_DATA_CALL] = function REQUEST_SETUP_DATA_CALL(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let version = Buf.readInt32(); + // Skip number of data calls. + Buf.readInt32(); + + this.readDataCall(options, version); + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SIM_IO] = function REQUEST_SIM_IO(length, options) { + if (options.errorMsg) { + if (options.onerror) { + options.onerror(options.errorMsg); + } + return; + } + + let Buf = this.context.Buf; + options.sw1 = Buf.readInt32(); + options.sw2 = Buf.readInt32(); + + // See 3GPP TS 11.11, clause 9.4.1 for operation success results. + if (options.sw1 !== ICC_STATUS_NORMAL_ENDING && + options.sw1 !== ICC_STATUS_NORMAL_ENDING_WITH_EXTRA && + options.sw1 !== ICC_STATUS_WITH_SIM_DATA && + options.sw1 !== ICC_STATUS_WITH_RESPONSE_DATA) { + if (DEBUG) { + this.context.debug("ICC I/O Error EF id = 0x" + options.fileId.toString(16) + + ", command = 0x" + options.command.toString(16) + + ", sw1 = 0x" + options.sw1.toString(16) + + ", sw2 = 0x" + options.sw2.toString(16)); + } + if (options.onerror) { + // We can get fail cause from sw1/sw2 (See TS 11.11 clause 9.4.1 and + // ISO 7816-4 clause 6). But currently no one needs this information, + // so simply reports "GenericFailure" for now. + options.onerror(GECKO_ERROR_GENERIC_FAILURE); + } + return; + } + this.context.ICCIOHelper.processICCIO(options); +}; +RilObject.prototype[REQUEST_SEND_USSD] = function REQUEST_SEND_USSD(length, options) { + if (DEBUG) { + this.context.debug("REQUEST_SEND_USSD " + JSON.stringify(options)); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CANCEL_USSD] = function REQUEST_CANCEL_USSD(length, options) { + if (DEBUG) { + this.context.debug("REQUEST_CANCEL_USSD" + JSON.stringify(options)); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_CLIR] = function REQUEST_GET_CLIR(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let bufLength = Buf.readInt32(); + if (!bufLength || bufLength < 2) { + options.errorMsg = GECKO_ERROR_GENERIC_FAILURE; + this.sendChromeMessage(options); + return; + } + + options.n = Buf.readInt32(); // Will be TS 27.007 +CLIR parameter 'n'. + options.m = Buf.readInt32(); // Will be TS 27.007 +CLIR parameter 'm'. + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_CLIR] = function REQUEST_SET_CLIR(length, options) { + if (options.rilMessageType == null) { + // The request was made by ril_worker itself automatically. Don't report. + return; + } + + this.sendChromeMessage(options); +}; + +RilObject.prototype[REQUEST_QUERY_CALL_FORWARD_STATUS] = + function REQUEST_QUERY_CALL_FORWARD_STATUS(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let rulesLength = 0; + if (length) { + rulesLength = Buf.readInt32(); + } + if (!rulesLength) { + options.errorMsg = GECKO_ERROR_GENERIC_FAILURE; + this.sendChromeMessage(options); + return; + } + let rules = new Array(rulesLength); + for (let i = 0; i < rulesLength; i++) { + let rule = {}; + rule.active = Buf.readInt32() == 1; // CALL_FORWARD_STATUS_* + rule.reason = Buf.readInt32(); // CALL_FORWARD_REASON_* + rule.serviceClass = Buf.readInt32(); + rule.toa = Buf.readInt32(); + rule.number = Buf.readString(); + rule.timeSeconds = Buf.readInt32(); + rules[i] = rule; + } + options.rules = rules; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_CALL_FORWARD] = + function REQUEST_SET_CALL_FORWARD(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_QUERY_CALL_WAITING] = + function REQUEST_QUERY_CALL_WAITING(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let results = Buf.readInt32List(); + let enabled = (results[0] === 1); + options.serviceClass = enabled ? results[1] : ICC_SERVICE_CLASS_NONE; + this.sendChromeMessage(options); +}; + +RilObject.prototype[REQUEST_SET_CALL_WAITING] = function REQUEST_SET_CALL_WAITING(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SMS_ACKNOWLEDGE] = null; +RilObject.prototype[REQUEST_GET_IMEI] = null; +RilObject.prototype[REQUEST_GET_IMEISV] = null; +RilObject.prototype[REQUEST_ANSWER] = function REQUEST_ANSWER(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_DEACTIVATE_DATA_CALL] = function REQUEST_DEACTIVATE_DATA_CALL(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_QUERY_FACILITY_LOCK] = function REQUEST_QUERY_FACILITY_LOCK(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + if (!length) { + options.errorMsg = GECKO_ERROR_GENERIC_FAILURE; + this.sendChromeMessage(options); + return; + } + + // Buf.readInt32List()[0] for Call Barring is a bit vector of services. + options.serviceClass = this.context.Buf.readInt32List()[0]; + if (options.queryServiceClass) { + options.enabled = (options.serviceClass & options.queryServiceClass) ? true : false; + options.serviceClass = options.queryServiceClass; + } else { + options.enabled = options.serviceClass ? true : false; + } + + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_FACILITY_LOCK] = function REQUEST_SET_FACILITY_LOCK(length, options) { + options.retryCount = length ? this.context.Buf.readInt32List()[0] : -1; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CHANGE_BARRING_PASSWORD] = + function REQUEST_CHANGE_BARRING_PASSWORD(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_QUERY_NETWORK_SELECTION_MODE] = function REQUEST_QUERY_NETWORK_SELECTION_MODE(length, options) { + this._receivedNetworkInfo(NETWORK_INFO_NETWORK_SELECTION_MODE); + + if (options.errorMsg) { + return; + } + + let mode = this.context.Buf.readInt32List(); + let selectionMode; + + switch (mode[0]) { + case NETWORK_SELECTION_MODE_AUTOMATIC: + selectionMode = GECKO_NETWORK_SELECTION_AUTOMATIC; + break; + case NETWORK_SELECTION_MODE_MANUAL: + selectionMode = GECKO_NETWORK_SELECTION_MANUAL; + break; + default: + selectionMode = GECKO_NETWORK_SELECTION_UNKNOWN; + break; + } + + this._updateNetworkSelectionMode(selectionMode); +}; +RilObject.prototype[REQUEST_SET_NETWORK_SELECTION_AUTOMATIC] = function REQUEST_SET_NETWORK_SELECTION_AUTOMATIC(length, options) { + if (!options.errorMsg) { + this._updateNetworkSelectionMode(GECKO_NETWORK_SELECTION_AUTOMATIC); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_NETWORK_SELECTION_MANUAL] = function REQUEST_SET_NETWORK_SELECTION_MANUAL(length, options) { + if (!options.errorMsg) { + this._updateNetworkSelectionMode(GECKO_NETWORK_SELECTION_MANUAL); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_QUERY_AVAILABLE_NETWORKS] = function REQUEST_QUERY_AVAILABLE_NETWORKS(length, options) { + if (!options.errorMsg) { + options.networks = this._processNetworks(); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_DTMF_START] = function REQUEST_DTMF_START(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_DTMF_STOP] = null; +RilObject.prototype[REQUEST_BASEBAND_VERSION] = function REQUEST_BASEBAND_VERSION(length, options) { + if (options.errorMsg) { + return; + } + + this.basebandVersion = this.context.Buf.readString(); + if (DEBUG) this.context.debug("Baseband version: " + this.basebandVersion); +}; +RilObject.prototype[REQUEST_SEPARATE_CONNECTION] = function REQUEST_SEPARATE_CONNECTION(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_SET_MUTE] = null; +RilObject.prototype[REQUEST_GET_MUTE] = null; +RilObject.prototype[REQUEST_QUERY_CLIP] = function REQUEST_QUERY_CLIP(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let bufLength = Buf.readInt32(); + if (!bufLength) { + options.errorMsg = GECKO_ERROR_GENERIC_FAILURE; + this.sendChromeMessage(options); + return; + } + + options.provisioned = Buf.readInt32(); + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_LAST_DATA_CALL_FAIL_CAUSE] = null; + +/** + * V6: + * # addresses - A space-delimited list of addresses with optional "/" prefix + * length. + * # dnses - A space-delimited list of DNS server addresses. + * # gateways - A space-delimited list of default gateway addresses. + * + * V10: + * # pcscf - A space-delimited list of Proxy Call State Control Function + * addresses. + */ + +RilObject.prototype.readDataCall = function(options, version) { + if (!options) { + options = {}; + } + let Buf = this.context.Buf; + options.failCause = Buf.readInt32(); // DATACALL_FAIL_* + options.suggestedRetryTime = Buf.readInt32(); + options.cid = Buf.readInt32().toString(); + options.active = Buf.readInt32(); // DATACALL_ACTIVE_* + options.type = Buf.readString(); + options.ifname = Buf.readString(); + options.addresses = Buf.readString(); + options.dnses = Buf.readString(); + options.gateways = Buf.readString(); + + if (version >= 10) { + options.pcscf = Buf.readString(); + } + + if (version >= 11) { + let mtu = Buf.readInt32(); + options.mtu = (mtu > 0) ? mtu : -1 ; + } + + return options; +}; + +RilObject.prototype[REQUEST_DATA_CALL_LIST] = function REQUEST_DATA_CALL_LIST(length, options) { + if (options.errorMsg) { + if (options.rilMessageType) { + this.sendChromeMessage(options); + } + return; + } + + if (!options.rilMessageType) { + // This is an unsolicited data call list changed. + options.rilMessageType = "datacalllistchanged"; + } + + if (!length) { + options.datacalls = []; + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let version = Buf.readInt32(); + let num = Buf.readInt32(); + let datacalls = []; + for (let i = 0; i < num; i++) { + let datacall; + datacall = this.readDataCall({}, version); + datacalls.push(datacall); + } + + options.datacalls = datacalls; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_RESET_RADIO] = null; +RilObject.prototype[REQUEST_OEM_HOOK_RAW] = null; +RilObject.prototype[REQUEST_OEM_HOOK_STRINGS] = null; +RilObject.prototype[REQUEST_SCREEN_STATE] = null; +RilObject.prototype[REQUEST_SET_SUPP_SVC_NOTIFICATION] = null; +RilObject.prototype[REQUEST_WRITE_SMS_TO_SIM] = function REQUEST_WRITE_SMS_TO_SIM(length, options) { + if (options.errorMsg) { + // `The MS shall return a "protocol error, unspecified" error message if + // the short message cannot be stored in the (U)SIM, and there is other + // message storage available at the MS` ~ 3GPP TS 23.038 section 4. Here + // we assume we always have indexed db as another storage. + this.acknowledgeGsmSms(false, PDU_FCS_PROTOCOL_ERROR); + } else { + this.acknowledgeGsmSms(true, PDU_FCS_OK); + } +}; +RilObject.prototype[REQUEST_DELETE_SMS_ON_SIM] = null; +RilObject.prototype[REQUEST_SET_BAND_MODE] = null; +RilObject.prototype[REQUEST_QUERY_AVAILABLE_BAND_MODE] = null; +RilObject.prototype[REQUEST_STK_GET_PROFILE] = null; +RilObject.prototype[REQUEST_STK_SET_PROFILE] = null; +RilObject.prototype[REQUEST_STK_SEND_ENVELOPE_COMMAND] = null; +RilObject.prototype[REQUEST_STK_SEND_TERMINAL_RESPONSE] = null; +RilObject.prototype[REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM] = null; +RilObject.prototype[REQUEST_EXPLICIT_CALL_TRANSFER] = null; +RilObject.prototype[REQUEST_SET_PREFERRED_NETWORK_TYPE] = function REQUEST_SET_PREFERRED_NETWORK_TYPE(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_PREFERRED_NETWORK_TYPE] = function REQUEST_GET_PREFERRED_NETWORK_TYPE(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + options.type = this.context.Buf.readInt32List()[0]; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_NEIGHBORING_CELL_IDS] = function REQUEST_GET_NEIGHBORING_CELL_IDS(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let radioTech = this.voiceRegistrationState.radioTech; + if (radioTech == undefined || radioTech == NETWORK_CREG_TECH_UNKNOWN) { + options.errorMsg = "RadioTechUnavailable"; + this.sendChromeMessage(options); + return; + } + if (!this._isGsmTechGroup(radioTech) || radioTech == NETWORK_CREG_TECH_LTE) { + options.errorMsg = "UnsupportedRadioTech"; + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let neighboringCellIds = []; + let num = Buf.readInt32(); + + for (let i = 0; i < num; i++) { + let cellId = {}; + cellId.networkType = GECKO_RADIO_TECH[radioTech]; + cellId.signalStrength = Buf.readInt32(); + + let cid = Buf.readString(); + // pad cid string with leading "0" + let length = cid.length; + if (length > 8) { + continue; + } + if (length < 8) { + for (let j = 0; j < (8-length); j++) { + cid = "0" + cid; + } + } + + switch (radioTech) { + case NETWORK_CREG_TECH_GPRS: + case NETWORK_CREG_TECH_EDGE: + case NETWORK_CREG_TECH_GSM: + cellId.gsmCellId = this.parseInt(cid.substring(4), -1, 16); + cellId.gsmLocationAreaCode = this.parseInt(cid.substring(0, 4), -1, 16); + break; + case NETWORK_CREG_TECH_UMTS: + case NETWORK_CREG_TECH_HSDPA: + case NETWORK_CREG_TECH_HSUPA: + case NETWORK_CREG_TECH_HSPA: + case NETWORK_CREG_TECH_HSPAP: + case NETWORK_CREG_TECH_DCHSPAP_1: + case NETWORK_CREG_TECH_DCHSPAP_2: + cellId.wcdmaPsc = this.parseInt(cid, -1, 16); + break; + } + + neighboringCellIds.push(cellId); + } + + options.result = neighboringCellIds; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_CELL_INFO_LIST] = function REQUEST_GET_CELL_INFO_LIST(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + let cellInfoList = []; + let num = Buf.readInt32(); + for (let i = 0; i < num; i++) { + let cellInfo = {}; + cellInfo.type = Buf.readInt32(); + cellInfo.registered = Buf.readInt32() ? true : false; + cellInfo.timestampType = Buf.readInt32(); + cellInfo.timestamp = Buf.readInt64(); + + switch(cellInfo.type) { + case CELL_INFO_TYPE_GSM: + case CELL_INFO_TYPE_WCDMA: + cellInfo.mcc = Buf.readInt32(); + cellInfo.mnc = Buf.readInt32(); + cellInfo.lac = Buf.readInt32(); + cellInfo.cid = Buf.readInt32(); + if (cellInfo.type == CELL_INFO_TYPE_WCDMA) { + cellInfo.psc = Buf.readInt32(); + } + cellInfo.signalStrength = Buf.readInt32(); + cellInfo.bitErrorRate = Buf.readInt32(); + break; + case CELL_INFO_TYPE_CDMA: + cellInfo.networkId = Buf.readInt32(); + cellInfo.systemId = Buf.readInt32(); + cellInfo.basestationId = Buf.readInt32(); + cellInfo.longitude = Buf.readInt32(); + cellInfo.latitude = Buf.readInt32(); + cellInfo.cdmaDbm = Buf.readInt32(); + cellInfo.cdmaEcio = Buf.readInt32(); + cellInfo.evdoDbm = Buf.readInt32(); + cellInfo.evdoEcio = Buf.readInt32(); + cellInfo.evdoSnr = Buf.readInt32(); + break; + case CELL_INFO_TYPE_LTE: + cellInfo.mcc = Buf.readInt32(); + cellInfo.mnc = Buf.readInt32(); + cellInfo.cid = Buf.readInt32(); + cellInfo.pcid = Buf.readInt32(); + cellInfo.tac = Buf.readInt32(); + cellInfo.signalStrength = Buf.readInt32(); + cellInfo.rsrp = Buf.readInt32(); + cellInfo.rsrq = Buf.readInt32(); + cellInfo.rssnr = Buf.readInt32(); + cellInfo.cqi = Buf.readInt32(); + break; + } + cellInfoList.push(cellInfo); + } + options.result = cellInfoList; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_LOCATION_UPDATES] = null; +RilObject.prototype[REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE] = null; +RilObject.prototype[REQUEST_CDMA_SET_ROAMING_PREFERENCE] = function REQUEST_CDMA_SET_ROAMING_PREFERENCE(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CDMA_QUERY_ROAMING_PREFERENCE] = function REQUEST_CDMA_QUERY_ROAMING_PREFERENCE(length, options) { + if (!options.errorMsg) { + options.mode = this.context.Buf.readInt32List()[0]; + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_TTY_MODE] = null; +RilObject.prototype[REQUEST_QUERY_TTY_MODE] = null; +RilObject.prototype[REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE] = function REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE] = function REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let enabled = this.context.Buf.readInt32List(); + options.enabled = enabled[0] ? true : false; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_CDMA_FLASH] = function REQUEST_CDMA_FLASH(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_CDMA_BURST_DTMF] = null; +RilObject.prototype[REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY] = null; +RilObject.prototype[REQUEST_CDMA_SEND_SMS] = function REQUEST_CDMA_SEND_SMS(length, options) { + this._processSmsSendResult(length, options); +}; +RilObject.prototype[REQUEST_CDMA_SMS_ACKNOWLEDGE] = null; +RilObject.prototype[REQUEST_GSM_GET_BROADCAST_SMS_CONFIG] = null; +RilObject.prototype[REQUEST_GSM_SET_BROADCAST_SMS_CONFIG] = function REQUEST_GSM_SET_BROADCAST_SMS_CONFIG(length, options) { + if (options.errorMsg) { + return; + } + this.setSmsBroadcastActivation(true); +}; +RilObject.prototype[REQUEST_GSM_SMS_BROADCAST_ACTIVATION] = null; +RilObject.prototype[REQUEST_CDMA_GET_BROADCAST_SMS_CONFIG] = null; +RilObject.prototype[REQUEST_CDMA_SET_BROADCAST_SMS_CONFIG] = function REQUEST_CDMA_SET_BROADCAST_SMS_CONFIG(length, options) { + if (options.errorMsg) { + return; + } + this.setSmsBroadcastActivation(true); +}; +RilObject.prototype[REQUEST_CDMA_SMS_BROADCAST_ACTIVATION] = null; +RilObject.prototype[REQUEST_CDMA_SUBSCRIPTION] = function REQUEST_CDMA_SUBSCRIPTION(length, options) { + if (options.errorMsg) { + return; + } + + let result = this.context.Buf.readStringList(); + + this.iccInfo.mdn = result[0]; + // The result[1] is Home SID. (Already be handled in readCDMAHome()) + // The result[2] is Home NID. (Already be handled in readCDMAHome()) + // The result[3] is MIN. + this.iccInfo.prlVersion = parseInt(result[4], 10); + + this.context.ICCUtilsHelper.handleICCInfoChange(); +}; +RilObject.prototype[REQUEST_CDMA_WRITE_SMS_TO_RUIM] = null; +RilObject.prototype[REQUEST_CDMA_DELETE_SMS_ON_RUIM] = null; +RilObject.prototype[REQUEST_DEVICE_IDENTITY] = function REQUEST_DEVICE_IDENTITY(length, options) { + if (options.errorMsg) { + this.context.debug("Failed to get device identities:" + options.errorMsg); + return; + } + + let result = this.context.Buf.readStringList(); + this.deviceIdentities = { + imei: result[0] || null, + imeisv: result[1] || null, + esn: result[2] || null, + meid: result[3] || null, + }; + + this.sendChromeMessage({ + rilMessageType: "deviceidentitieschange", + deviceIdentities: this.deviceIdentities + }); +}; +RilObject.prototype[REQUEST_EXIT_EMERGENCY_CALLBACK_MODE] = function REQUEST_EXIT_EMERGENCY_CALLBACK_MODE(length, options) { + if (options.internal) { + return; + } + + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_SMSC_ADDRESS] = function REQUEST_GET_SMSC_ADDRESS(length, options) { + if (!options.rilMessageType || options.rilMessageType !== "getSmscAddress") { + return; + } + + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let tosca = TOA_UNKNOWN; + let smsc = ""; + let Buf = this.context.Buf; + if (RILQUIRKS_SMSC_ADDRESS_FORMAT === "pdu") { + let pduHelper = this.context.GsmPDUHelper; + let strlen = Buf.readInt32(); + let length = pduHelper.readHexOctet(); + + // As defined in |8.2.5.2 Destination address element| of 3GPP TS 24.011, + // the value of length field can not exceed 11. Since the content might be + // filled with 12 'F' when SMSC is cleared, we don't parse the TOA and + // address fields if reported length exceeds 11 here. Instead, keep the + // default value (TOA_UNKNOWN with an empty address) in this case. + const MAX_LENGTH = 11 + if (length <= MAX_LENGTH) { + tosca = pduHelper.readHexOctet(); + + // Read and covert the decimal values back to special BCD digits defined in + // |Called party BCD number| of 3GPP TS 24.008 (refer the following table). + // + // +=========+=======+=====+ + // | value | digit | hex | + // +======================== + // | 1 0 1 0 | * | 0xA | + // | 1 0 1 1 | # | 0xB | + // | 1 1 0 0 | a | 0xC | + // | 1 1 0 1 | b | 0xD | + // | 1 1 1 0 | c | 0xE | + // +=========+=======+=====+ + smsc = pduHelper.readSwappedNibbleBcdString(length - 1, true) + .replace(/a/ig, "*") + .replace(/b/ig, "#") + .replace(/c/ig, "a") + .replace(/d/ig, "b") + .replace(/e/ig, "c"); + + Buf.readStringDelimiter(strlen); + } + } else /* RILQUIRKS_SMSC_ADDRESS_FORMAT === "text" */ { + let text = Buf.readString(); + let segments = text.split(",", 2); + // Parse TOA only if it presents since some devices might omit the TOA + // segment in the reported SMSC address. If TOA does not present, keep the + // default value TOA_UNKNOWN. + if (segments.length === 2) { + tosca = this.parseInt(segments[1], TOA_UNKNOWN, 10); + } + + smsc = segments[0].replace(/\"/g, ""); + } + + // Convert the NPI value to the corresponding index of CALLED_PARTY_BCD_NPI + // array. If the value does not present in the array, use + // CALLED_PARTY_BCD_NPI_ISDN. + let npi = CALLED_PARTY_BCD_NPI.indexOf(tosca & 0xf); + if (npi === -1) { + npi = CALLED_PARTY_BCD_NPI.indexOf(CALLED_PARTY_BCD_NPI_ISDN); + } + + // Extract TON. + let ton = (tosca & 0x70) >> 4; + + // Ensure + sign if TON is international, and vice versa. + const TON_INTERNATIONAL = (TOA_INTERNATIONAL & 0x70) >> 4; + if (ton === TON_INTERNATIONAL && smsc.charAt(0) !== "+") { + smsc = "+" + smsc; + } else if (smsc.charAt(0) === "+" && ton !== TON_INTERNATIONAL) { + if (DEBUG) { + this.context.debug("SMSC address number begins with '+' while the TON is not international. Change TON to international."); + } + ton = TON_INTERNATIONAL; + } + + options.smscAddress = smsc; + options.typeOfNumber = ton; + options.numberPlanIdentification = npi; + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SET_SMSC_ADDRESS] = function REQUEST_SET_SMSC_ADDRESS(length, options) { + if (!options.rilMessageType || options.rilMessageType !== "setSmscAddress") { + return; + } + + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_REPORT_SMS_MEMORY_STATUS] = function REQUEST_REPORT_SMS_MEMORY_STATUS(length, options) { + this.pendingToReportSmsMemoryStatus = !!options.errorMsg; +}; +RilObject.prototype[REQUEST_REPORT_STK_SERVICE_IS_RUNNING] = null; +RilObject.prototype[REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE] = null; +RilObject.prototype[REQUEST_ISIM_AUTHENTICATION] = null; +RilObject.prototype[REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU] = null; +RilObject.prototype[REQUEST_STK_SEND_ENVELOPE_WITH_STATUS] = function REQUEST_STK_SEND_ENVELOPE_WITH_STATUS(length, options) { + if (options.errorMsg) { + this.acknowledgeGsmSms(false, PDU_FCS_UNSPECIFIED); + return; + } + + let Buf = this.context.Buf; + let sw1 = Buf.readInt32(); + let sw2 = Buf.readInt32(); + if ((sw1 == ICC_STATUS_SAT_BUSY) && (sw2 === 0x00)) { + this.acknowledgeGsmSms(false, PDU_FCS_USAT_BUSY); + return; + } + + let success = ((sw1 == ICC_STATUS_NORMAL_ENDING) && (sw2 === 0x00)) + || (sw1 == ICC_STATUS_NORMAL_ENDING_WITH_EXTRA); + + let messageStringLength = Buf.readInt32(); // In semi-octets + let responsePduLen = messageStringLength / 2; // In octets + if (!responsePduLen) { + this.acknowledgeGsmSms(success, success ? PDU_FCS_OK + : PDU_FCS_USIM_DATA_DOWNLOAD_ERROR); + return; + } + + this.acknowledgeIncomingGsmSmsWithPDU(success, responsePduLen, options); +}; +RilObject.prototype[REQUEST_VOICE_RADIO_TECH] = function REQUEST_VOICE_RADIO_TECH(length, options) { + if (options.errorMsg) { + if (DEBUG) { + this.context.debug("Error when getting voice radio tech: " + + options.errorMsg); + } + return; + } + let radioTech = this.context.Buf.readInt32List(); + this._processRadioTech(radioTech[0]); +}; +RilObject.prototype[REQUEST_SET_UNSOL_CELL_INFO_LIST_RATE] = null; +RilObject.prototype[REQUEST_SET_INITIAL_ATTACH_APN] = null; +RilObject.prototype[REQUEST_IMS_REGISTRATION_STATE] = null; +RilObject.prototype[REQUEST_IMS_SEND_SMS] = null; +RilObject.prototype[REQUEST_SIM_TRANSMIT_APDU_BASIC] = null; +RilObject.prototype[REQUEST_SIM_OPEN_CHANNEL] = function REQUEST_SIM_OPEN_CHANNEL(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + options.channel = this.context.Buf.readInt32List()[0]; + // onwards may optionally contain the select response for the open channel + // command with one byte per integer. + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_SIM_CLOSE_CHANNEL] = function REQUEST_SIM_CLOSE_CHANNEL(length, options) { + this.sendDefaultResponse(options); +}; +RilObject.prototype[REQUEST_SIM_TRANSMIT_APDU_CHANNEL] = function REQUEST_SIM_TRANSMIT_APDU_CHANNEL(length, options) { + if (options.errorMsg) { + this.sendChromeMessage(options); + return; + } + + let Buf = this.context.Buf; + options.sw1 = Buf.readInt32(); + options.sw2 = Buf.readInt32(); + options.simResponse = Buf.readString(); + if (DEBUG) { + this.context.debug("Setting return values for RIL[REQUEST_SIM_TRANSMIT_APDU_CHANNEL]: [" + + options.sw1 + "," + + options.sw2 + ", " + + options.simResponse + "]"); + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_NV_READ_ITEM] = null; +RilObject.prototype[REQUEST_NV_WRITE_ITEM] = null; +RilObject.prototype[REQUEST_NV_WRITE_CDMA_PRL] = null; +RilObject.prototype[REQUEST_NV_RESET_CONFIG] = null; +RilObject.prototype[REQUEST_SET_UICC_SUBSCRIPTION] = function REQUEST_SET_UICC_SUBSCRIPTION(length, options) { + // Resend data subscription after uicc subscription. + if (this._attachDataRegistration) { + this.setDataRegistration({attach: true}); + } +}; +RilObject.prototype[REQUEST_ALLOW_DATA] = null; +RilObject.prototype[REQUEST_GET_HARDWARE_CONFIG] = null; +RilObject.prototype[REQUEST_SIM_AUTHENTICATION] = null; +RilObject.prototype[REQUEST_GET_DC_RT_INFO] = null; +RilObject.prototype[REQUEST_SET_DC_RT_INFO_RATE] = null; +RilObject.prototype[REQUEST_SET_DATA_PROFILE] = null; +RilObject.prototype[REQUEST_SHUTDOWN] = null; +RilObject.prototype[REQUEST_SET_DATA_SUBSCRIPTION] = function REQUEST_SET_DATA_SUBSCRIPTION(length, options) { + if (!options.rilMessageType) { + // The request was made by ril_worker itself. Don't report. + return; + } + this.sendChromeMessage(options); +}; +RilObject.prototype[REQUEST_GET_UNLOCK_RETRY_COUNT] = function REQUEST_GET_UNLOCK_RETRY_COUNT(length, options) { + options.retryCount = length ? this.context.Buf.readInt32List()[0] : -1; + this.sendChromeMessage(options); +}; +RilObject.prototype[RIL_REQUEST_GPRS_ATTACH] = function RIL_REQUEST_GPRS_ATTACH(length, options) { + if (!options.rilMessageType) { + // The request was made by ril_worker itself. Don't report. + return; + } + this.sendChromeMessage(options); +}; +RilObject.prototype[RIL_REQUEST_GPRS_DETACH] = function RIL_REQUEST_GPRS_DETACH(length, options) { + this.sendChromeMessage(options); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_RADIO_STATE_CHANGED] = function UNSOLICITED_RESPONSE_RADIO_STATE_CHANGED() { + let radioState = this.context.Buf.readInt32(); + let newState; + switch (radioState) { + case RADIO_STATE_UNAVAILABLE: + newState = GECKO_RADIOSTATE_UNKNOWN; + break; + case RADIO_STATE_OFF: + newState = GECKO_RADIOSTATE_DISABLED; + break; + default: + newState = GECKO_RADIOSTATE_ENABLED; + } + + if (DEBUG) { + this.context.debug("Radio state changed from '" + this.radioState + + "' to '" + newState + "'"); + } + if (this.radioState == newState) { + return; + } + + if (radioState !== RADIO_STATE_UNAVAILABLE) { + // Retrieve device identities once radio is available. + this.getDeviceIdentity(); + } + + if (radioState == RADIO_STATE_ON) { + // This value is defined in RIL v7, we will retrieve radio tech by another + // request. We leave _isCdma untouched, and it will be set once we get the + // radio technology. + this._waitingRadioTech = true; + this.getVoiceRadioTechnology(); + } + + if ((this.radioState == GECKO_RADIOSTATE_UNKNOWN || + this.radioState == GECKO_RADIOSTATE_DISABLED) && + newState == GECKO_RADIOSTATE_ENABLED) { + // The radio became available, let's get its info. + this.getBasebandVersion(); + this.updateCellBroadcastConfig(); + if ((RILQUIRKS_DATA_REGISTRATION_ON_DEMAND || + RILQUIRKS_SUBSCRIPTION_CONTROL) && + this._attachDataRegistration) { + this.setDataRegistration({attach: true}); + } + + if (this.pendingToReportSmsMemoryStatus) { + this._updateSmsMemoryStatus(); + } + } + + this.radioState = newState; + this.sendChromeMessage({ + rilMessageType: "radiostatechange", + radioState: newState + }); + + // If the radio is up and on, so let's query the card state. + // On older RILs only if the card is actually ready, though. + // If _waitingRadioTech is set, we don't need to get icc status now. + if (radioState == RADIO_STATE_UNAVAILABLE || + radioState == RADIO_STATE_OFF || + this._waitingRadioTech) { + return; + } + this.getICCStatus(); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_CALL_STATE_CHANGED] = function UNSOLICITED_RESPONSE_CALL_STATE_CHANGED() { + this.getCurrentCalls(); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_VOICE_NETWORK_STATE_CHANGED] = function UNSOLICITED_RESPONSE_VOICE_NETWORK_STATE_CHANGED() { + if (DEBUG) { + this.context.debug("Network state changed, re-requesting phone state and " + + "ICC status"); + } + this.getICCStatus(); + this.requestNetworkInfo(); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_NEW_SMS] = function UNSOLICITED_RESPONSE_NEW_SMS(length) { + let [message, result] = this.context.GsmPDUHelper.processReceivedSms(length); + + if (message) { + result = this._processSmsMultipart(message); + } + + if (result == PDU_FCS_RESERVED || result == MOZ_FCS_WAIT_FOR_EXPLICIT_ACK) { + return; + } + + // Not reserved FCS values, send ACK now. + this.acknowledgeGsmSms(result == PDU_FCS_OK, result); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_NEW_SMS_STATUS_REPORT] = function UNSOLICITED_RESPONSE_NEW_SMS_STATUS_REPORT(length) { + let result = this._processSmsStatusReport(length); + this.acknowledgeGsmSms(result == PDU_FCS_OK, result); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_NEW_SMS_ON_SIM] = function UNSOLICITED_RESPONSE_NEW_SMS_ON_SIM(length) { + let recordNumber = this.context.Buf.readInt32List()[0]; + + this.context.SimRecordHelper.readSMS( + recordNumber, + function onsuccess(message) { + if (message && message.simStatus === 3) { //New Unread SMS + this._processSmsMultipart(message); + } + }.bind(this), + function onerror(errorMsg) { + if (DEBUG) { + this.context.debug("Failed to Read NEW SMS on SIM #" + recordNumber + + ", errorMsg: " + errorMsg); + } + }); +}; +RilObject.prototype[UNSOLICITED_ON_USSD] = function UNSOLICITED_ON_USSD() { + let [typeCode, message] = this.context.Buf.readStringList(); + if (DEBUG) { + this.context.debug("On USSD. Type Code: " + typeCode + " Message: " + message); + } + + this.sendChromeMessage({rilMessageType: "ussdreceived", + message: message, + // Per ril.h the USSD session is assumed to persist if + // the type code is "1", otherwise the current session + // (if any) is assumed to have terminated. + sessionEnded: typeCode !== "1"}); +}; +RilObject.prototype[UNSOLICITED_ON_USSD_REQUEST] = null; +RilObject.prototype[UNSOLICITED_NITZ_TIME_RECEIVED] = function UNSOLICITED_NITZ_TIME_RECEIVED() { + let dateString = this.context.Buf.readString(); + + // The data contained in the NITZ message is + // in the form "yy/mm/dd,hh:mm:ss(+/-)tz,dt" + // for example: 12/02/16,03:36:08-20,00,310410 + // See also bug 714352 - Listen for NITZ updates from rild. + + if (DEBUG) this.context.debug("DateTimeZone string " + dateString); + + let now = Date.now(); + + let year = parseInt(dateString.substr(0, 2), 10); + let month = parseInt(dateString.substr(3, 2), 10); + let day = parseInt(dateString.substr(6, 2), 10); + let hours = parseInt(dateString.substr(9, 2), 10); + let minutes = parseInt(dateString.substr(12, 2), 10); + let seconds = parseInt(dateString.substr(15, 2), 10); + // Note that |tz| is in 15-min units. + let tz = parseInt(dateString.substr(17, 3), 10); + // Note that |dst| is in 1-hour units and is already applied in |tz|. + let dst = parseInt(dateString.substr(21, 2), 10); + + let timeInMS = Date.UTC(year + PDU_TIMESTAMP_YEAR_OFFSET, month - 1, day, + hours, minutes, seconds); + + if (isNaN(timeInMS)) { + if (DEBUG) this.context.debug("NITZ failed to convert date"); + return; + } + + this.sendChromeMessage({rilMessageType: "nitzTime", + networkTimeInMS: timeInMS, + networkTimeZoneInMinutes: -(tz * 15), + networkDSTInMinutes: -(dst * 60), + receiveTimeInMS: now}); +}; + +RilObject.prototype[UNSOLICITED_SIGNAL_STRENGTH] = function UNSOLICITED_SIGNAL_STRENGTH(length) { + this[REQUEST_SIGNAL_STRENGTH](length, {}); +}; +RilObject.prototype[UNSOLICITED_DATA_CALL_LIST_CHANGED] = function UNSOLICITED_DATA_CALL_LIST_CHANGED(length) { + this[REQUEST_DATA_CALL_LIST](length, {}); +}; +RilObject.prototype[UNSOLICITED_SUPP_SVC_NOTIFICATION] = function UNSOLICITED_SUPP_SVC_NOTIFICATION(length) { + let Buf = this.context.Buf; + let info = {}; + info.notificationType = Buf.readInt32(); + info.code = Buf.readInt32(); + info.index = Buf.readInt32(); + info.type = Buf.readInt32(); + info.number = Buf.readString(); + + this._processSuppSvcNotification(info); +}; + +RilObject.prototype[UNSOLICITED_STK_SESSION_END] = function UNSOLICITED_STK_SESSION_END() { + this.sendChromeMessage({rilMessageType: "stksessionend"}); +}; +RilObject.prototype[UNSOLICITED_STK_PROACTIVE_COMMAND] = function UNSOLICITED_STK_PROACTIVE_COMMAND() { + this.processStkProactiveCommand(); +}; +RilObject.prototype[UNSOLICITED_STK_EVENT_NOTIFY] = function UNSOLICITED_STK_EVENT_NOTIFY() { + this.processStkProactiveCommand(); +}; +RilObject.prototype[UNSOLICITED_STK_CALL_SETUP] = null; +RilObject.prototype[UNSOLICITED_SIM_SMS_STORAGE_FULL] = null; +RilObject.prototype[UNSOLICITED_SIM_REFRESH] = null; +RilObject.prototype[UNSOLICITED_CALL_RING] = function UNSOLICITED_CALL_RING() { + let Buf = this.context.Buf; + let info = {rilMessageType: "callRing"}; + let isCDMA = false; //XXX TODO hard-code this for now + if (isCDMA) { + info.isPresent = Buf.readInt32(); + info.signalType = Buf.readInt32(); + info.alertPitch = Buf.readInt32(); + info.signal = Buf.readInt32(); + } + // At this point we don't know much other than the fact there's an incoming + // call, but that's enough to bring up the Phone app already. We'll know + // details once we get a call state changed notification and can then + // dispatch DOM events etc. + this.sendChromeMessage(info); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_SIM_STATUS_CHANGED] = function UNSOLICITED_RESPONSE_SIM_STATUS_CHANGED() { + this.getICCStatus(); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_CDMA_NEW_SMS] = function UNSOLICITED_RESPONSE_CDMA_NEW_SMS(length) { + let [message, result] = this.context.CdmaPDUHelper.processReceivedSms(length); + + if (message) { + if (message.teleservice === PDU_CDMA_MSG_TELESERIVCIE_ID_WAP) { + result = this._processCdmaSmsWapPush(message); + } else if (message.subMsgType === PDU_CDMA_MSG_TYPE_DELIVER_ACK) { + result = this._processCdmaSmsStatusReport(message); + } else { + result = this._processSmsMultipart(message); + } + } + + if (result == PDU_FCS_RESERVED || result == MOZ_FCS_WAIT_FOR_EXPLICIT_ACK) { + return; + } + + // Not reserved FCS values, send ACK now. + this.acknowledgeCdmaSms(result == PDU_FCS_OK, result); +}; +RilObject.prototype[UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS] = function UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS(length) { + let message; + try { + message = + this.context.GsmPDUHelper.readCbMessage(this.context.Buf.readInt32()); + + // "Data-Download" message is expected to be handled by the modem. + // Ignore it here to prevent any garbage messages to be displayed. + // See 9.4.1.2.2 Message Identifier of TS 32.041 for the range of + // Message-identifier of the Data-Download CB messages. + if (message.messageId >= 0x1000 && message.messageId <= 0x10FF) { + if (DEBUG) { + this.context.debug("Ignore a Data-Download message, messageId: " + + message.messageId); + } + return; + } + } catch (e) { + if (DEBUG) { + this.context.debug("Failed to parse Cell Broadcast message: " + e); + } + return; + } + + message = this._processReceivedSmsCbPage(message); + if (!message) { + return; + } + + // Bug 1235697, failed to deactivate CBS in some modem. + // Workaround it according to the settings. + // Note: ETWS/CMAS/PWS can be received even disabled. + // It will be displayed according to the setting in application layer. + if (this.cellBroadcastDisabled && ( + !(message.messageId >= 0x1100 && message.messageId <= 0x1107) && // ETWS + !(message.messageId >= 0x1112 && message.messageId <= 0x112F) && // CMAS + !(message.messageId >= 0x1130 && message.messageId <= 0x18FF) // PWS + )) { + if (DEBUG) { + this.context.debug("Ignore a CB message when disabled, messageId: " + + message.messageId); + } + return; + } + + message.rilMessageType = "cellbroadcast-received"; + this.sendChromeMessage(message); +}; +RilObject.prototype[UNSOLICITED_CDMA_RUIM_SMS_STORAGE_FULL] = null; +RilObject.prototype[UNSOLICITED_RESTRICTED_STATE_CHANGED] = null; +RilObject.prototype[UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE] = function UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE() { + this._handleChangedEmergencyCbMode(true); +}; +RilObject.prototype[UNSOLICITED_CDMA_CALL_WAITING] = function UNSOLICITED_CDMA_CALL_WAITING(length) { + let Buf = this.context.Buf; + let call = {}; + call.number = Buf.readString(); + call.numberPresentation = Buf.readInt32(); + call.name = Buf.readString(); + call.namePresentation = Buf.readInt32(); + call.isPresent = Buf.readInt32(); + call.signalType = Buf.readInt32(); + call.alertPitch = Buf.readInt32(); + call.signal = Buf.readInt32(); + this.sendChromeMessage({rilMessageType: "cdmaCallWaiting", + waitingCall: call}); +}; +RilObject.prototype[UNSOLICITED_CDMA_OTA_PROVISION_STATUS] = function UNSOLICITED_CDMA_OTA_PROVISION_STATUS() { + let status = + CDMA_OTA_PROVISION_STATUS_TO_GECKO[this.context.Buf.readInt32List()[0]]; + if (!status) { + return; + } + this.sendChromeMessage({rilMessageType: "otastatuschange", + status: status}); +}; +RilObject.prototype[UNSOLICITED_CDMA_INFO_REC] = function UNSOLICITED_CDMA_INFO_REC(length) { + this.sendChromeMessage({ + rilMessageType: "cdma-info-rec-received", + records: this.context.CdmaPDUHelper.decodeInformationRecord() + }); +}; +RilObject.prototype[UNSOLICITED_OEM_HOOK_RAW] = null; +RilObject.prototype[UNSOLICITED_RINGBACK_TONE] = null; +RilObject.prototype[UNSOLICITED_RESEND_INCALL_MUTE] = null; +RilObject.prototype[UNSOLICITED_CDMA_SUBSCRIPTION_SOURCE_CHANGED] = null; +RilObject.prototype[UNSOLICITED_CDMA_PRL_CHANGED] = function UNSOLICITED_CDMA_PRL_CHANGED(length) { + let version = this.context.Buf.readInt32List()[0]; + if (version !== this.iccInfo.prlVersion) { + this.iccInfo.prlVersion = version; + this.context.ICCUtilsHelper.handleICCInfoChange(); + } +}; +RilObject.prototype[UNSOLICITED_EXIT_EMERGENCY_CALLBACK_MODE] = function UNSOLICITED_EXIT_EMERGENCY_CALLBACK_MODE() { + this._handleChangedEmergencyCbMode(false); +}; +RilObject.prototype[UNSOLICITED_RIL_CONNECTED] = function UNSOLICITED_RIL_CONNECTED(length) { + // Prevent response id collision between UNSOLICITED_RIL_CONNECTED and + // UNSOLICITED_VOICE_RADIO_TECH_CHANGED for Akami on gingerbread branch. + if (!length) { + return; + } + + this.version = this.context.Buf.readInt32List()[0]; + if (DEBUG) { + this.context.debug("Detected RIL version " + this.version); + } + + this.initRILState(); + // rild might have restarted, ensure data call list. + this.getDataCallList(); + // Always ensure that we are not in emergency callback mode when init. + this.exitEmergencyCbMode(); + // Reset radio in the case that b2g restart (or crash). + this.setRadioEnabled({enabled: false}); +}; +RilObject.prototype[UNSOLICITED_VOICE_RADIO_TECH_CHANGED] = function UNSOLICITED_VOICE_RADIO_TECH_CHANGED(length) { + // This unsolicited response will be sent when the technology of a multi-tech + // modem is changed, ex. switch between gsm and cdma. + // TODO: We may need to do more on updating data when switching between gsm + // and cdma mode, e.g. IMEI, ESN, iccInfo, iccType ... etc. + // See Bug 866038. + this._processRadioTech(this.context.Buf.readInt32List()[0]); +}; +RilObject.prototype[UNSOLICITED_CELL_INFO_LIST] = null; +RilObject.prototype[UNSOLICITED_RESPONSE_IMS_NETWORK_STATE_CHANGED] = null; +RilObject.prototype[UNSOLICITED_UICC_SUBSCRIPTION_STATUS_CHANGED] = null; +RilObject.prototype[UNSOLICITED_SRVCC_STATE_NOTIFY] = null; +RilObject.prototype[UNSOLICITED_HARDWARE_CONFIG_CHANGED] = null; +RilObject.prototype[UNSOLICITED_DC_RT_INFO_CHANGED] = null; + +/** + * This object exposes the functionality to parse and serialize PDU strings + * + * A PDU is a string containing a series of hexadecimally encoded octets + * or nibble-swapped binary-coded decimals (BCDs). It contains not only the + * message text but information about the sender, the SMS service center, + * timestamp, etc. + */ +function GsmPDUHelperObject(aContext) { + this.context = aContext; +} +GsmPDUHelperObject.prototype = { + context: null, + + /** + * Read one character (2 bytes) from a RIL string and decode as hex. + * + * @return the nibble as a number. + */ + readHexNibble: function() { + let nibble = this.context.Buf.readUint16(); + if (nibble >= 48 && nibble <= 57) { + nibble -= 48; // ASCII '0'..'9' + } else if (nibble >= 65 && nibble <= 70) { + nibble -= 55; // ASCII 'A'..'F' + } else if (nibble >= 97 && nibble <= 102) { + nibble -= 87; // ASCII 'a'..'f' + } else { + throw "Found invalid nibble during PDU parsing: " + + String.fromCharCode(nibble); + } + return nibble; + }, + + /** + * Encode a nibble as one hex character in a RIL string (2 bytes). + * + * @param nibble + * The nibble to encode (represented as a number) + */ + writeHexNibble: function(nibble) { + nibble &= 0x0f; + if (nibble < 10) { + nibble += 48; // ASCII '0' + } else { + nibble += 55; // ASCII 'A' + } + this.context.Buf.writeUint16(nibble); + }, + + /** + * Read a hex-encoded octet (two nibbles). + * + * @return the octet as a number. + */ + readHexOctet: function() { + return (this.readHexNibble() << 4) | this.readHexNibble(); + }, + + /** + * Write an octet as two hex-encoded nibbles. + * + * @param octet + * The octet (represented as a number) to encode. + */ + writeHexOctet: function(octet) { + this.writeHexNibble(octet >> 4); + this.writeHexNibble(octet); + }, + + /** + * Read an array of hex-encoded octets. + */ + readHexOctetArray: function(length) { + let array = new Uint8Array(length); + for (let i = 0; i < length; i++) { + array[i] = this.readHexOctet(); + } + return array; + }, + + /** + * Helper to write data into a temporary buffer for easier length encoding when + * the number of octets for the length encoding is varied. + * + * @param writeFunction + * Function of how the data to be written into temporary buffer. + * + * @return array of written octets. + **/ + writeWithBuffer: function(writeFunction) { + let buf = []; + let writeHexOctet = this.writeHexOctet; + this.writeHexOctet = function(octet) { + buf.push(octet); + } + + try { + writeFunction(); + } catch (e) { + if (DEBUG) { + debug("Error when writeWithBuffer: " + e); + } + buf = []; + } finally { + this.writeHexOctet = writeHexOctet; + } + + return buf; + }, + + /** + * Convert an octet (number) to a BCD number. + * + * Any nibbles that are not in the BCD range count as 0. + * + * @param octet + * The octet (a number, as returned by getOctet()) + * + * @return the corresponding BCD number. + */ + octetToBCD: function(octet) { + return ((octet & 0xf0) <= 0x90) * ((octet >> 4) & 0x0f) + + ((octet & 0x0f) <= 0x09) * (octet & 0x0f) * 10; + }, + + /** + * Convert a BCD number to an octet (number) + * + * Only take two digits with absolute value. + * + * @param bcd + * + * @return the corresponding octet. + */ + BCDToOctet: function(bcd) { + bcd = Math.abs(bcd); + return ((bcd % 10) << 4) + (Math.floor(bcd / 10) % 10); + }, + + /** + * Convert a semi-octet (number) to a GSM BCD char, or return empty + * string if invalid semiOctet and suppressException is set to true. + * + * @param semiOctet + * Nibble to be converted to. + * @param suppressException [optional] + * Suppress exception if invalid semiOctet and suppressException is set + * to true. + * + * @return GSM BCD char, or empty string. + */ + bcdChars: "0123456789", + semiOctetToBcdChar: function(semiOctet, suppressException) { + if (semiOctet >= this.bcdChars.length) { + if (suppressException) { + return ""; + } else { + throw new RangeError(); + } + } + + return this.bcdChars.charAt(semiOctet); + }, + + /** + * Convert a semi-octet (number) to a GSM extended BCD char, or return empty + * string if invalid semiOctet and suppressException is set to true. + * + * @param semiOctet + * Nibble to be converted to. + * @param suppressException [optional] + * Suppress exception if invalid semiOctet and suppressException is set + * to true. + * + * @return GSM extended BCD char, or empty string. + */ + extendedBcdChars: "0123456789*#,;", + semiOctetToExtendedBcdChar: function(semiOctet, suppressException) { + if (semiOctet >= this.extendedBcdChars.length) { + if (suppressException) { + return ""; + } else { + throw new RangeError(); + } + } + + return this.extendedBcdChars.charAt(semiOctet); + }, + + /** + * Convert string to a GSM extended BCD string + */ + stringToExtendedBcd: function(string) { + return string.replace(/[^0-9*#,]/g, "") + .replace(/\*/g, "a") + .replace(/\#/g, "b") + .replace(/\,/g, "c"); + }, + + /** + * Read a *swapped nibble* binary coded decimal (BCD) + * + * @param pairs + * Number of nibble *pairs* to read. + * + * @return the decimal as a number. + */ + readSwappedNibbleBcdNum: function(pairs) { + let number = 0; + for (let i = 0; i < pairs; i++) { + let octet = this.readHexOctet(); + // Ignore 'ff' octets as they're often used as filler. + if (octet == 0xff) { + continue; + } + // If the first nibble is an "F" , only the second nibble is to be taken + // into account. + if ((octet & 0xf0) == 0xf0) { + number *= 10; + number += octet & 0x0f; + continue; + } + number *= 100; + number += this.octetToBCD(octet); + } + return number; + }, + + /** + * Read a *swapped nibble* binary coded decimal (BCD) string + * + * @param pairs + * Number of nibble *pairs* to read. + * @param suppressException [optional] + * Suppress exception if invalid semiOctet and suppressException is set + * to true. + * + * @return The BCD string. + */ + readSwappedNibbleBcdString: function(pairs, suppressException) { + let str = ""; + for (let i = 0; i < pairs; i++) { + let nibbleH = this.readHexNibble(); + let nibbleL = this.readHexNibble(); + if (nibbleL == 0x0F) { + break; + } + + str += this.semiOctetToBcdChar(nibbleL, suppressException); + if (nibbleH != 0x0F) { + str += this.semiOctetToBcdChar(nibbleH, suppressException); + } + } + + return str; + }, + + /** + * Read a *swapped nibble* extended binary coded decimal (BCD) string + * + * @param pairs + * Number of nibble *pairs* to read. + * @param suppressException [optional] + * Suppress exception if invalid semiOctet and suppressException is set + * to true. + * + * @return The BCD string. + */ + readSwappedNibbleExtendedBcdString: function(pairs, suppressException) { + let str = ""; + for (let i = 0; i < pairs; i++) { + let nibbleH = this.readHexNibble(); + let nibbleL = this.readHexNibble(); + if (nibbleL == 0x0F) { + break; + } + + str += this.semiOctetToExtendedBcdChar(nibbleL, suppressException); + if (nibbleH != 0x0F) { + str += this.semiOctetToExtendedBcdChar(nibbleH, suppressException); + } + } + + return str; + }, + + /** + * Write numerical data as swapped nibble BCD. + * + * @param data + * Data to write (as a string or a number) + */ + writeSwappedNibbleBCD: function(data) { + data = data.toString(); + if (data.length % 2) { + data += "F"; + } + let Buf = this.context.Buf; + for (let i = 0; i < data.length; i += 2) { + Buf.writeUint16(data.charCodeAt(i + 1)); + Buf.writeUint16(data.charCodeAt(i)); + } + }, + + /** + * Write numerical data as swapped nibble BCD. + * If the number of digit of data is even, add '0' at the beginning. + * + * @param data + * Data to write (as a string or a number) + */ + writeSwappedNibbleBCDNum: function(data) { + data = data.toString(); + if (data.length % 2) { + data = "0" + data; + } + let Buf = this.context.Buf; + for (let i = 0; i < data.length; i += 2) { + Buf.writeUint16(data.charCodeAt(i + 1)); + Buf.writeUint16(data.charCodeAt(i)); + } + }, + + /** + * Read user data, convert to septets, look up relevant characters in a + * 7-bit alphabet, and construct string. + * + * @param length + * Number of septets to read (*not* octets) + * @param paddingBits + * Number of padding bits in the first byte of user data. + * @param langIndex + * Table index used for normal 7-bit encoded character lookup. + * @param langShiftIndex + * Table index used for escaped 7-bit encoded character lookup. + * + * @return a string. + */ + readSeptetsToString: function(length, paddingBits, langIndex, langShiftIndex) { + let ret = ""; + let byteLength = Math.ceil((length * 7 + paddingBits) / 8); + + /** + * |<- last byte in header ->| + * |<- incompleteBits ->|<- last header septet->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + * + * |<- 1st byte in user data ->| + * |<- data septet 1 ->|<-paddingBits->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + * + * |<- 2nd byte in user data ->| + * |<- data spetet 2 ->|<-ds1->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + */ + let data = 0; + let dataBits = 0; + if (paddingBits) { + data = this.readHexOctet() >> paddingBits; + dataBits = 8 - paddingBits; + --byteLength; + } + + let escapeFound = false; + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[langIndex]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[langShiftIndex]; + do { + // Read as much as fits in 32bit word + let bytesToRead = Math.min(byteLength, dataBits ? 3 : 4); + for (let i = 0; i < bytesToRead; i++) { + data |= this.readHexOctet() << dataBits; + dataBits += 8; + --byteLength; + } + + // Consume available full septets + for (; dataBits >= 7; dataBits -= 7) { + let septet = data & 0x7F; + data >>>= 7; + + if (escapeFound) { + escapeFound = false; + if (septet == PDU_NL_EXTENDED_ESCAPE) { + // According to 3GPP TS 23.038, section 6.2.1.1, NOTE 1, "On + // receipt of this code, a receiving entity shall display a space + // until another extensiion table is defined." + ret += " "; + } else if (septet == PDU_NL_RESERVED_CONTROL) { + // According to 3GPP TS 23.038 B.2, "This code represents a control + // character and therefore must not be used for language specific + // characters." + ret += " "; + } else { + ret += langShiftTable[septet]; + } + } else if (septet == PDU_NL_EXTENDED_ESCAPE) { + escapeFound = true; + + // <escape> is not an effective character + --length; + } else { + ret += langTable[septet]; + } + } + } while (byteLength); + + if (ret.length != length) { + /** + * If num of effective characters does not equal to the length of read + * string, cut the tail off. This happens when the last octet of user + * data has following layout: + * + * |<- penultimate octet in user data ->| + * |<- data septet N ->|<- dsN-1 ->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + * + * |<- last octet in user data ->| + * |<- fill bits ->|<-dsN->| + * +===7===|===6===|===5===|===4===|===3===|===2===|===1===|===0===| + * + * The fill bits in the last octet may happen to form a full septet and + * be appended at the end of result string. + */ + ret = ret.slice(0, length); + } + return ret; + }, + + writeStringAsSeptets: function(message, paddingBits, langIndex, langShiftIndex) { + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[langIndex]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[langShiftIndex]; + + let dataBits = paddingBits; + let data = 0; + for (let i = 0; i < message.length; i++) { + let c = message.charAt(i); + let septet = langTable.indexOf(c); + if (septet == PDU_NL_EXTENDED_ESCAPE) { + continue; + } + + if (septet >= 0) { + data |= septet << dataBits; + dataBits += 7; + } else { + septet = langShiftTable.indexOf(c); + if (septet == -1) { + throw new Error("'" + c + "' is not in 7 bit alphabet " + + langIndex + ":" + langShiftIndex + "!"); + } + + if (septet == PDU_NL_RESERVED_CONTROL) { + continue; + } + + data |= PDU_NL_EXTENDED_ESCAPE << dataBits; + dataBits += 7; + data |= septet << dataBits; + dataBits += 7; + } + + for (; dataBits >= 8; dataBits -= 8) { + this.writeHexOctet(data & 0xFF); + data >>>= 8; + } + } + + if (dataBits !== 0) { + this.writeHexOctet(data & 0xFF); + } + }, + + writeStringAs8BitUnpacked: function(text) { + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + let len = text ? text.length : 0; + for (let i = 0; i < len; i++) { + let c = text.charAt(i); + let octet = langTable.indexOf(c); + + if (octet == -1) { + octet = langShiftTable.indexOf(c); + if (octet == -1) { + // Fallback to ASCII space. + octet = langTable.indexOf(' '); + } else { + this.writeHexOctet(PDU_NL_EXTENDED_ESCAPE); + } + } + this.writeHexOctet(octet); + } + }, + + /** + * Read user data and decode as a UCS2 string. + * + * @param numOctets + * Number of octets to be read as UCS2 string. + * + * @return a string. + */ + readUCS2String: function(numOctets) { + let str = ""; + let length = numOctets / 2; + for (let i = 0; i < length; ++i) { + let code = (this.readHexOctet() << 8) | this.readHexOctet(); + str += String.fromCharCode(code); + } + + if (DEBUG) this.context.debug("Read UCS2 string: " + str); + + return str; + }, + + /** + * Write user data as a UCS2 string. + * + * @param message + * Message string to encode as UCS2 in hex-encoded octets. + */ + writeUCS2String: function(message) { + for (let i = 0; i < message.length; ++i) { + let code = message.charCodeAt(i); + this.writeHexOctet((code >> 8) & 0xFF); + this.writeHexOctet(code & 0xFF); + } + }, + + /** + * Read 1 + UDHL octets and construct user data header. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.040 9.2.3.24 + */ + readUserDataHeader: function(msg) { + /** + * A header object with properties contained in received message. + * The properties set include: + * + * length: totoal length of the header, default 0. + * langIndex: used locking shift table index, default + * PDU_NL_IDENTIFIER_DEFAULT. + * langShiftIndex: used locking shift table index, default + * PDU_NL_IDENTIFIER_DEFAULT. + * + */ + let header = { + length: 0, + langIndex: PDU_NL_IDENTIFIER_DEFAULT, + langShiftIndex: PDU_NL_IDENTIFIER_DEFAULT + }; + + header.length = this.readHexOctet(); + if (DEBUG) this.context.debug("Read UDH length: " + header.length); + + let dataAvailable = header.length; + while (dataAvailable >= 2) { + let id = this.readHexOctet(); + let length = this.readHexOctet(); + if (DEBUG) this.context.debug("Read UDH id: " + id + ", length: " + length); + + dataAvailable -= 2; + + switch (id) { + case PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT: { + let ref = this.readHexOctet(); + let max = this.readHexOctet(); + let seq = this.readHexOctet(); + dataAvailable -= 3; + if (max && seq && (seq <= max)) { + header.segmentRef = ref; + header.segmentMaxSeq = max; + header.segmentSeq = seq; + } + break; + } + case PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_8BIT: { + let dstp = this.readHexOctet(); + let orip = this.readHexOctet(); + dataAvailable -= 2; + if ((dstp < PDU_APA_RESERVED_8BIT_PORTS) + || (orip < PDU_APA_RESERVED_8BIT_PORTS)) { + // 3GPP TS 23.040 clause 9.2.3.24.3: "A receiving entity shall + // ignore any information element where the value of the + // Information-Element-Data is Reserved or not supported" + break; + } + header.destinationPort = dstp; + header.originatorPort = orip; + break; + } + case PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_16BIT: { + let dstp = (this.readHexOctet() << 8) | this.readHexOctet(); + let orip = (this.readHexOctet() << 8) | this.readHexOctet(); + dataAvailable -= 4; + if ((dstp >= PDU_APA_VALID_16BIT_PORTS) || + (orip >= PDU_APA_VALID_16BIT_PORTS)) { + // 3GPP TS 23.040 clause 9.2.3.24.4: "A receiving entity shall + // ignore any information element where the value of the + // Information-Element-Data is Reserved or not supported" + // Bug 1130292, some carriers set originatorPort to reserved port + // numbers for wap push. We rise this as a warning in debug message + // instead of ingoring this IEI to allow user to receive Wap Push + // under these carriers. + this.context.debug("Warning: Invalid port numbers [dstp, orip]: " + + JSON.stringify([dstp, orip])); + } + header.destinationPort = dstp; + header.originatorPort = orip; + break; + } + case PDU_IEI_CONCATENATED_SHORT_MESSAGES_16BIT: { + let ref = (this.readHexOctet() << 8) | this.readHexOctet(); + let max = this.readHexOctet(); + let seq = this.readHexOctet(); + dataAvailable -= 4; + if (max && seq && (seq <= max)) { + header.segmentRef = ref; + header.segmentMaxSeq = max; + header.segmentSeq = seq; + } + break; + } + case PDU_IEI_NATIONAL_LANGUAGE_SINGLE_SHIFT: + let langShiftIndex = this.readHexOctet(); + --dataAvailable; + if (langShiftIndex < PDU_NL_SINGLE_SHIFT_TABLES.length) { + header.langShiftIndex = langShiftIndex; + } + break; + case PDU_IEI_NATIONAL_LANGUAGE_LOCKING_SHIFT: + let langIndex = this.readHexOctet(); + --dataAvailable; + if (langIndex < PDU_NL_LOCKING_SHIFT_TABLES.length) { + header.langIndex = langIndex; + } + break; + case PDU_IEI_SPECIAL_SMS_MESSAGE_INDICATION: + let msgInd = this.readHexOctet() & 0xFF; + let msgCount = this.readHexOctet(); + dataAvailable -= 2; + + + /* + * TS 23.040 V6.8.1 Sec 9.2.3.24.2 + * bits 1 0 : basic message indication type + * bits 4 3 2 : extended message indication type + * bits 6 5 : Profile id + * bit 7 : storage type + */ + let storeType = msgInd & PDU_MWI_STORE_TYPE_BIT; + let mwi = msg.mwi; + if (!mwi) { + mwi = msg.mwi = {}; + } + + if (storeType == PDU_MWI_STORE_TYPE_STORE) { + // Store message because TP_UDH indicates so, note this may override + // the setting in DCS, but that is expected + mwi.discard = false; + } else if (mwi.discard === undefined) { + // storeType == PDU_MWI_STORE_TYPE_DISCARD + // only override mwi.discard here if it hasn't already been set + mwi.discard = true; + } + + mwi.msgCount = msgCount & 0xFF; + mwi.active = mwi.msgCount > 0; + + if (DEBUG) { + this.context.debug("MWI in TP_UDH received: " + JSON.stringify(mwi)); + } + + break; + default: + if (DEBUG) { + this.context.debug("readUserDataHeader: unsupported IEI(" + id + + "), " + length + " bytes."); + } + + // Read out unsupported data + if (length) { + let octets; + if (DEBUG) octets = new Uint8Array(length); + + for (let i = 0; i < length; i++) { + let octet = this.readHexOctet(); + if (DEBUG) octets[i] = octet; + } + dataAvailable -= length; + + if (DEBUG) { + this.context.debug("readUserDataHeader: " + Array.slice(octets)); + } + } + break; + } + } + + if (dataAvailable !== 0) { + throw new Error("Illegal user data header found!"); + } + + msg.header = header; + }, + + /** + * Write out user data header. + * + * @param options + * Options containing information for user data header write-out. The + * `userDataHeaderLength` property must be correctly pre-calculated. + */ + writeUserDataHeader: function(options) { + this.writeHexOctet(options.userDataHeaderLength); + + if (options.segmentMaxSeq > 1) { + if (options.segmentRef16Bit) { + this.writeHexOctet(PDU_IEI_CONCATENATED_SHORT_MESSAGES_16BIT); + this.writeHexOctet(4); + this.writeHexOctet((options.segmentRef >> 8) & 0xFF); + } else { + this.writeHexOctet(PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT); + this.writeHexOctet(3); + } + this.writeHexOctet(options.segmentRef & 0xFF); + this.writeHexOctet(options.segmentMaxSeq & 0xFF); + this.writeHexOctet(options.segmentSeq & 0xFF); + } + + if (options.dcs == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + if (options.langIndex != PDU_NL_IDENTIFIER_DEFAULT) { + this.writeHexOctet(PDU_IEI_NATIONAL_LANGUAGE_LOCKING_SHIFT); + this.writeHexOctet(1); + this.writeHexOctet(options.langIndex); + } + + if (options.langShiftIndex != PDU_NL_IDENTIFIER_DEFAULT) { + this.writeHexOctet(PDU_IEI_NATIONAL_LANGUAGE_SINGLE_SHIFT); + this.writeHexOctet(1); + this.writeHexOctet(options.langShiftIndex); + } + } + }, + + /** + * Read SM-TL Address. + * + * @param len + * Length of useful semi-octets within the Address-Value field. For + * example, the lenth of "12345" should be 5, and 4 for "1234". + * + * @see 3GPP TS 23.040 9.1.2.5 + */ + readAddress: function(len) { + // Address Length + if (!len || (len < 0)) { + if (DEBUG) { + this.context.debug("PDU error: invalid sender address length: " + len); + } + return null; + } + if (len % 2 == 1) { + len += 1; + } + if (DEBUG) this.context.debug("PDU: Going to read address: " + len); + + // Type-of-Address + let toa = this.readHexOctet(); + let addr = ""; + + if ((toa & 0xF0) == PDU_TOA_ALPHANUMERIC) { + addr = this.readSeptetsToString(Math.floor(len * 4 / 7), 0, + PDU_NL_IDENTIFIER_DEFAULT , PDU_NL_IDENTIFIER_DEFAULT ); + return addr; + } + addr = this.readSwappedNibbleExtendedBcdString(len / 2); + if (addr.length <= 0) { + if (DEBUG) this.context.debug("PDU error: no number provided"); + return null; + } + if ((toa & 0xF0) == (PDU_TOA_INTERNATIONAL)) { + addr = '+' + addr; + } + + return addr; + }, + + /** + * Read TP-Protocol-Indicator(TP-PID). + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.040 9.2.3.9 + */ + readProtocolIndicator: function(msg) { + // `The MS shall interpret reserved, obsolete, or unsupported values as the + // value 00000000 but shall store them exactly as received.` + msg.pid = this.readHexOctet(); + + msg.epid = msg.pid; + switch (msg.epid & 0xC0) { + case 0x40: + // Bit 7..0 = 01xxxxxx + switch (msg.epid) { + case PDU_PID_SHORT_MESSAGE_TYPE_0: + case PDU_PID_ANSI_136_R_DATA: + case PDU_PID_USIM_DATA_DOWNLOAD: + return; + } + break; + } + + msg.epid = PDU_PID_DEFAULT; + }, + + /** + * Read TP-Data-Coding-Scheme(TP-DCS) + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.040 9.2.3.10, 3GPP TS 23.038 4. + */ + readDataCodingScheme: function(msg) { + let dcs = this.readHexOctet(); + if (DEBUG) this.context.debug("PDU: read SMS dcs: " + dcs); + + // No message class by default. + let messageClass = PDU_DCS_MSG_CLASS_NORMAL; + // 7 bit is the default fallback encoding. + let encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; + switch (dcs & PDU_DCS_CODING_GROUP_BITS) { + case 0x40: // bits 7..4 = 01xx + case 0x50: + case 0x60: + case 0x70: + // Bit 5..0 are coded exactly the same as Group 00xx + case 0x00: // bits 7..4 = 00xx + case 0x10: + case 0x20: + case 0x30: + if (dcs & 0x10) { + messageClass = dcs & PDU_DCS_MSG_CLASS_BITS; + } + switch (dcs & 0x0C) { + case 0x4: + encoding = PDU_DCS_MSG_CODING_8BITS_ALPHABET; + break; + case 0x8: + encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET; + break; + } + break; + + case 0xE0: // bits 7..4 = 1110 + encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET; + // Bit 3..0 are coded exactly the same as Message Waiting Indication + // Group 1101. + // Fall through. + case 0xC0: // bits 7..4 = 1100 + case 0xD0: // bits 7..4 = 1101 + // Indiciates voicemail indicator set or clear + let active = (dcs & PDU_DCS_MWI_ACTIVE_BITS) == PDU_DCS_MWI_ACTIVE_VALUE; + + // If TP-UDH is present, these values will be overwritten + switch (dcs & PDU_DCS_MWI_TYPE_BITS) { + case PDU_DCS_MWI_TYPE_VOICEMAIL: + let mwi = msg.mwi; + if (!mwi) { + mwi = msg.mwi = {}; + } + + mwi.active = active; + mwi.discard = (dcs & PDU_DCS_CODING_GROUP_BITS) == 0xC0; + mwi.msgCount = active ? GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN : 0; + + if (DEBUG) { + this.context.debug("MWI in DCS received for voicemail: " + + JSON.stringify(mwi)); + } + break; + case PDU_DCS_MWI_TYPE_FAX: + if (DEBUG) this.context.debug("MWI in DCS received for fax"); + break; + case PDU_DCS_MWI_TYPE_EMAIL: + if (DEBUG) this.context.debug("MWI in DCS received for email"); + break; + default: + if (DEBUG) this.context.debug("MWI in DCS received for \"other\""); + break; + } + break; + + case 0xF0: // bits 7..4 = 1111 + if (dcs & 0x04) { + encoding = PDU_DCS_MSG_CODING_8BITS_ALPHABET; + } + messageClass = dcs & PDU_DCS_MSG_CLASS_BITS; + break; + + default: + // Falling back to default encoding. + break; + } + + msg.dcs = dcs; + msg.encoding = encoding; + msg.messageClass = GECKO_SMS_MESSAGE_CLASSES[messageClass]; + + if (DEBUG) this.context.debug("PDU: message encoding is " + encoding + " bit."); + }, + + /** + * Read GSM TP-Service-Centre-Time-Stamp(TP-SCTS). + * + * @see 3GPP TS 23.040 9.2.3.11 + */ + readTimestamp: function() { + let year = this.readSwappedNibbleBcdNum(1) + PDU_TIMESTAMP_YEAR_OFFSET; + let month = this.readSwappedNibbleBcdNum(1) - 1; + let day = this.readSwappedNibbleBcdNum(1); + let hour = this.readSwappedNibbleBcdNum(1); + let minute = this.readSwappedNibbleBcdNum(1); + let second = this.readSwappedNibbleBcdNum(1); + let timestamp = Date.UTC(year, month, day, hour, minute, second); + + // If the most significant bit of the least significant nibble is 1, + // the timezone offset is negative (fourth bit from the right => 0x08): + // localtime = UTC + tzOffset + // therefore + // UTC = localtime - tzOffset + let tzOctet = this.readHexOctet(); + let tzOffset = this.octetToBCD(tzOctet & ~0x08) * 15 * 60 * 1000; + tzOffset = (tzOctet & 0x08) ? -tzOffset : tzOffset; + timestamp -= tzOffset; + + return timestamp; + }, + + /** + * Write GSM TP-Service-Centre-Time-Stamp(TP-SCTS). + * + * @see 3GPP TS 23.040 9.2.3.11 + */ + writeTimestamp: function(date) { + this.writeSwappedNibbleBCDNum(date.getFullYear() - PDU_TIMESTAMP_YEAR_OFFSET); + + // The value returned by getMonth() is an integer between 0 and 11. + // 0 is corresponds to January, 1 to February, and so on. + this.writeSwappedNibbleBCDNum(date.getMonth() + 1); + this.writeSwappedNibbleBCDNum(date.getDate()); + this.writeSwappedNibbleBCDNum(date.getHours()); + this.writeSwappedNibbleBCDNum(date.getMinutes()); + this.writeSwappedNibbleBCDNum(date.getSeconds()); + + // the value returned by getTimezoneOffset() is the difference, + // in minutes, between UTC and local time. + // For example, if your time zone is UTC+10 (Australian Eastern Standard Time), + // -600 will be returned. + // In TS 23.040 9.2.3.11, the Time Zone field of TP-SCTS indicates + // the different between the local time and GMT. + // And expressed in quarters of an hours. (so need to divid by 15) + let zone = date.getTimezoneOffset() / 15; + let octet = this.BCDToOctet(zone); + + // the bit3 of the Time Zone field represents the algebraic sign. + // (0: positive, 1: negative). + // For example, if the time zone is -0800 GMT, + // 480 will be returned by getTimezoneOffset(). + // In this case, need to mark sign bit as 1. => 0x08 + if (zone > 0) { + octet = octet | 0x08; + } + this.writeHexOctet(octet); + }, + + /** + * User data can be 7 bit (default alphabet) data, 8 bit data, or 16 bit + * (UCS2) data. + * + * @param msg + * message object for output. + * @param length + * length of user data to read in octets. + */ + readUserData: function(msg, length) { + if (DEBUG) { + this.context.debug("Reading " + length + " bytes of user data."); + } + + let paddingBits = 0; + if (msg.udhi) { + this.readUserDataHeader(msg); + + if (msg.encoding == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + let headerBits = (msg.header.length + 1) * 8; + let headerSeptets = Math.ceil(headerBits / 7); + + length -= headerSeptets; + paddingBits = headerSeptets * 7 - headerBits; + } else { + length -= (msg.header.length + 1); + } + } + + if (DEBUG) { + this.context.debug("After header, " + length + " septets left of user data"); + } + + msg.body = null; + msg.data = null; + + if (length <= 0) { + // No data to read. + return; + } + + switch (msg.encoding) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + // 7 bit encoding allows 140 octets, which means 160 characters + // ((140x8) / 7 = 160 chars) + if (length > PDU_MAX_USER_DATA_7BIT) { + if (DEBUG) { + this.context.debug("PDU error: user data is too long: " + length); + } + break; + } + + let langIndex = msg.udhi ? msg.header.langIndex : PDU_NL_IDENTIFIER_DEFAULT; + let langShiftIndex = msg.udhi ? msg.header.langShiftIndex : PDU_NL_IDENTIFIER_DEFAULT; + msg.body = this.readSeptetsToString(length, paddingBits, langIndex, + langShiftIndex); + break; + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + msg.data = this.readHexOctetArray(length); + break; + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + msg.body = this.readUCS2String(length); + break; + } + }, + + /** + * Read extra parameters if TP-PI is set. + * + * @param msg + * message object for output. + */ + readExtraParams: function(msg) { + // Because each PDU octet is converted to two UCS2 char2, we should always + // get even messageStringLength in this#_processReceivedSms(). So, we'll + // always need two delimitors at the end. + if (this.context.Buf.getReadAvailable() <= 4) { + return; + } + + // TP-Parameter-Indicator + let pi; + do { + // `The most significant bit in octet 1 and any other TP-PI octets which + // may be added later is reserved as an extension bit which when set to a + // 1 shall indicate that another TP-PI octet follows immediately + // afterwards.` ~ 3GPP TS 23.040 9.2.3.27 + pi = this.readHexOctet(); + } while (pi & PDU_PI_EXTENSION); + + // `If the TP-UDL bit is set to "1" but the TP-DCS bit is set to "0" then + // the receiving entity shall for TP-DCS assume a value of 0x00, i.e. the + // 7bit default alphabet.` ~ 3GPP 23.040 9.2.3.27 + msg.dcs = 0; + msg.encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; + + // TP-Protocol-Identifier + if (pi & PDU_PI_PROTOCOL_IDENTIFIER) { + this.readProtocolIndicator(msg); + } + // TP-Data-Coding-Scheme + if (pi & PDU_PI_DATA_CODING_SCHEME) { + this.readDataCodingScheme(msg); + } + // TP-User-Data-Length + if (pi & PDU_PI_USER_DATA_LENGTH) { + let userDataLength = this.readHexOctet(); + this.readUserData(msg, userDataLength); + } + }, + + /** + * Read and decode a PDU-encoded message from the stream. + * + * TODO: add some basic sanity checks like: + * - do we have the minimum number of chars available + */ + readMessage: function() { + // An empty message object. This gets filled below and then returned. + let msg = { + // D:DELIVER, DR:DELIVER-REPORT, S:SUBMIT, SR:SUBMIT-REPORT, + // ST:STATUS-REPORT, C:COMMAND + // M:Mandatory, O:Optional, X:Unavailable + // D DR S SR ST C + SMSC: null, // M M M M M M + mti: null, // M M M M M M + udhi: null, // M M O M M M + sender: null, // M X X X X X + recipient: null, // X X M X M M + pid: null, // M O M O O M + epid: null, // M O M O O M + dcs: null, // M O M O O X + mwi: null, // O O O O O O + replace: false, // O O O O O O + header: null, // M M O M M M + body: null, // M O M O O O + data: null, // M O M O O O + sentTimestamp: null, // M X X X X X + status: null, // X X X X M X + scts: null, // X X X M M X + dt: null, // X X X X M X + }; + + // SMSC info + let smscLength = this.readHexOctet(); + if (smscLength > 0) { + let smscTypeOfAddress = this.readHexOctet(); + // Subtract the type-of-address octet we just read from the length. + msg.SMSC = this.readSwappedNibbleExtendedBcdString(smscLength - 1); + if ((smscTypeOfAddress >> 4) == (PDU_TOA_INTERNATIONAL >> 4)) { + msg.SMSC = '+' + msg.SMSC; + } + } + + // First octet of this SMS-DELIVER or SMS-SUBMIT message + let firstOctet = this.readHexOctet(); + // Message Type Indicator + msg.mti = firstOctet & 0x03; + // User data header indicator + msg.udhi = firstOctet & PDU_UDHI; + + switch (msg.mti) { + case PDU_MTI_SMS_RESERVED: + // `If an MS receives a TPDU with a "Reserved" value in the TP-MTI it + // shall process the message as if it were an "SMS-DELIVER" but store + // the message exactly as received.` ~ 3GPP TS 23.040 9.2.3.1 + case PDU_MTI_SMS_DELIVER: + return this.readDeliverMessage(msg); + case PDU_MTI_SMS_STATUS_REPORT: + return this.readStatusReportMessage(msg); + default: + return null; + } + }, + + /** + * Helper for processing received SMS parcel data. + * + * @param length + * Length of SMS string in the incoming parcel. + * + * @return Message parsed or null for invalid message. + */ + processReceivedSms: function(length) { + if (!length) { + if (DEBUG) this.context.debug("Received empty SMS!"); + return [null, PDU_FCS_UNSPECIFIED]; + } + + let Buf = this.context.Buf; + + // An SMS is a string, but we won't read it as such, so let's read the + // string length and then defer to PDU parsing helper. + let messageStringLength = Buf.readInt32(); + if (DEBUG) this.context.debug("Got new SMS, length " + messageStringLength); + let message = this.readMessage(); + if (DEBUG) this.context.debug("Got new SMS: " + JSON.stringify(message)); + + // Read string delimiters. See Buf.readString(). + Buf.readStringDelimiter(length); + + // Determine result + if (!message) { + return [null, PDU_FCS_UNSPECIFIED]; + } + + if (message.epid == PDU_PID_SHORT_MESSAGE_TYPE_0) { + // `A short message type 0 indicates that the ME must acknowledge receipt + // of the short message but shall discard its contents.` ~ 3GPP TS 23.040 + // 9.2.3.9 + return [null, PDU_FCS_OK]; + } + + if (message.messageClass == GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_2]) { + let RIL = this.context.RIL; + switch (message.epid) { + case PDU_PID_ANSI_136_R_DATA: + case PDU_PID_USIM_DATA_DOWNLOAD: + let ICCUtilsHelper = this.context.ICCUtilsHelper; + if (ICCUtilsHelper.isICCServiceAvailable("DATA_DOWNLOAD_SMS_PP")) { + // `If the service "data download via SMS Point-to-Point" is + // allocated and activated in the (U)SIM Service Table, ... then the + // ME shall pass the message transparently to the UICC using the + // ENVELOPE (SMS-PP DOWNLOAD).` ~ 3GPP TS 31.111 7.1.1.1 + RIL.dataDownloadViaSMSPP(message); + + // `the ME shall not display the message, or alert the user of a + // short message waiting.` ~ 3GPP TS 31.111 7.1.1.1 + return [null, PDU_FCS_RESERVED]; + } + + // If the service "data download via SMS-PP" is not available in the + // (U)SIM Service Table, ..., then the ME shall store the message in + // EFsms in accordance with TS 31.102` ~ 3GPP TS 31.111 7.1.1.1 + + // Fall through. + default: + RIL.writeSmsToSIM(message); + break; + } + } + + // TODO: Bug 739143: B2G SMS: Support SMS Storage Full event + if ((message.messageClass != GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]) && !true) { + // `When a mobile terminated message is class 0..., the MS shall display + // the message immediately and send a ACK to the SC ..., irrespective of + // whether there is memory available in the (U)SIM or ME.` ~ 3GPP 23.038 + // clause 4. + + if (message.messageClass == GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_2]) { + // `If all the short message storage at the MS is already in use, the + // MS shall return "memory capacity exceeded".` ~ 3GPP 23.038 clause 4. + return [null, PDU_FCS_MEMORY_CAPACITY_EXCEEDED]; + } + + return [null, PDU_FCS_UNSPECIFIED]; + } + + return [message, PDU_FCS_OK]; + }, + + /** + * Read and decode a SMS-DELIVER PDU. + * + * @param msg + * message object for output. + */ + readDeliverMessage: function(msg) { + // - Sender Address info - + let senderAddressLength = this.readHexOctet(); + msg.sender = this.readAddress(senderAddressLength); + // - TP-Protocolo-Identifier - + this.readProtocolIndicator(msg); + // - TP-Data-Coding-Scheme - + this.readDataCodingScheme(msg); + // - TP-Service-Center-Time-Stamp - + msg.sentTimestamp = this.readTimestamp(); + // - TP-User-Data-Length - + let userDataLength = this.readHexOctet(); + + // - TP-User-Data - + if (userDataLength > 0) { + this.readUserData(msg, userDataLength); + } + + return msg; + }, + + /** + * Read and decode a SMS-STATUS-REPORT PDU. + * + * @param msg + * message object for output. + */ + readStatusReportMessage: function(msg) { + // TP-Message-Reference + msg.messageRef = this.readHexOctet(); + // TP-Recipient-Address + let recipientAddressLength = this.readHexOctet(); + msg.recipient = this.readAddress(recipientAddressLength); + // TP-Service-Centre-Time-Stamp + msg.scts = this.readTimestamp(); + // TP-Discharge-Time + msg.dt = this.readTimestamp(); + // TP-Status + msg.status = this.readHexOctet(); + + this.readExtraParams(msg); + + return msg; + }, + + /** + * Serialize a SMS-SUBMIT PDU message and write it to the output stream. + * + * This method expects that a data coding scheme has been chosen already + * and that the length of the user data payload in that encoding is known, + * too. Both go hand in hand together anyway. + * + * @param address + * String containing the address (number) of the SMS receiver + * @param userData + * String containing the message to be sent as user data + * @param dcs + * Data coding scheme. One of the PDU_DCS_MSG_CODING_*BITS_ALPHABET + * constants. + * @param userDataHeaderLength + * Length of embedded user data header, in bytes. The whole header + * size will be userDataHeaderLength + 1; 0 for no header. + * @param encodedBodyLength + * Length of the user data when encoded with the given DCS. For UCS2, + * in bytes; for 7-bit, in septets. + * @param langIndex + * Table index used for normal 7-bit encoded character lookup. + * @param langShiftIndex + * Table index used for escaped 7-bit encoded character lookup. + * @param requestStatusReport + * Request status report. + */ + writeMessage: function(options) { + if (DEBUG) { + this.context.debug("writeMessage: " + JSON.stringify(options)); + } + let Buf = this.context.Buf; + let address = options.number; + let body = options.body; + let dcs = options.dcs; + let userDataHeaderLength = options.userDataHeaderLength; + let encodedBodyLength = options.encodedBodyLength; + let langIndex = options.langIndex; + let langShiftIndex = options.langShiftIndex; + + // SMS-SUBMIT Format: + // + // PDU Type - 1 octet + // Message Reference - 1 octet + // DA - Destination Address - 2 to 12 octets + // PID - Protocol Identifier - 1 octet + // DCS - Data Coding Scheme - 1 octet + // VP - Validity Period - 0, 1 or 7 octets + // UDL - User Data Length - 1 octet + // UD - User Data - 140 octets + + let addressFormat = PDU_TOA_ISDN; // 81 + if (address[0] == '+') { + addressFormat = PDU_TOA_INTERNATIONAL | PDU_TOA_ISDN; // 91 + address = address.substring(1); + } + //TODO validity is unsupported for now + let validity = 0; + + let headerOctets = (userDataHeaderLength ? userDataHeaderLength + 1 : 0); + let paddingBits; + let userDataLengthInSeptets; + let userDataLengthInOctets; + if (dcs == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + let headerSeptets = Math.ceil(headerOctets * 8 / 7); + userDataLengthInSeptets = headerSeptets + encodedBodyLength; + userDataLengthInOctets = Math.ceil(userDataLengthInSeptets * 7 / 8); + paddingBits = headerSeptets * 7 - headerOctets * 8; + } else { + userDataLengthInOctets = headerOctets + encodedBodyLength; + paddingBits = 0; + } + + let pduOctetLength = 4 + // PDU Type, Message Ref, address length + format + Math.ceil(address.length / 2) + + 3 + // PID, DCS, UDL + userDataLengthInOctets; + if (validity) { + //TODO: add more to pduOctetLength + } + + // Start the string. Since octets are represented in hex, we will need + // twice as many characters as octets. + Buf.writeInt32(pduOctetLength * 2); + + // - PDU-TYPE- + + // +--------+----------+---------+---------+--------+---------+ + // | RP (1) | UDHI (1) | SRR (1) | VPF (2) | RD (1) | MTI (2) | + // +--------+----------+---------+---------+--------+---------+ + // RP: 0 Reply path parameter is not set + // 1 Reply path parameter is set + // UDHI: 0 The UD Field contains only the short message + // 1 The beginning of the UD field contains a header in addition + // of the short message + // SRR: 0 A status report is not requested + // 1 A status report is requested + // VPF: bit4 bit3 + // 0 0 VP field is not present + // 0 1 Reserved + // 1 0 VP field present an integer represented (relative) + // 1 1 VP field present a semi-octet represented (absolute) + // RD: Instruct the SMSC to accept(0) or reject(1) an SMS-SUBMIT + // for a short message still held in the SMSC which has the same + // MR and DA as a previously submitted short message from the + // same OA + // MTI: bit1 bit0 Message Type + // 0 0 SMS-DELIVER (SMSC ==> MS) + // 0 1 SMS-SUBMIT (MS ==> SMSC) + + // PDU type. MTI is set to SMS-SUBMIT + let firstOctet = PDU_MTI_SMS_SUBMIT; + + // Status-Report-Request + if (options.requestStatusReport) { + firstOctet |= PDU_SRI_SRR; + } + + // Validity period + if (validity) { + //TODO: not supported yet, OR with one of PDU_VPF_* + } + // User data header indicator + if (headerOctets) { + firstOctet |= PDU_UDHI; + } + this.writeHexOctet(firstOctet); + + // Message reference 00 + this.writeHexOctet(0x00); + + // - Destination Address - + this.writeHexOctet(address.length); + this.writeHexOctet(addressFormat); + this.writeSwappedNibbleBCD(address); + + // - Protocol Identifier - + this.writeHexOctet(0x00); + + // - Data coding scheme - + // For now it assumes bits 7..4 = 1111 except for the 16 bits use case + this.writeHexOctet(dcs); + + // - Validity Period - + if (validity) { + this.writeHexOctet(validity); + } + + // - User Data - + if (dcs == PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + this.writeHexOctet(userDataLengthInSeptets); + } else { + this.writeHexOctet(userDataLengthInOctets); + } + + if (headerOctets) { + this.writeUserDataHeader(options); + } + + switch (dcs) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + this.writeStringAsSeptets(body, paddingBits, langIndex, langShiftIndex); + break; + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + // Unsupported. + break; + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + this.writeUCS2String(body); + break; + } + + // End of the string. The string length is always even by definition, so + // we write two \0 delimiters. + Buf.writeUint16(0); + Buf.writeUint16(0); + }, + + /** + * Read GSM CBS message serial number. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.1.2.1 + */ + readCbSerialNumber: function(msg) { + let Buf = this.context.Buf; + msg.serial = Buf.readUint8() << 8 | Buf.readUint8(); + msg.geographicalScope = (msg.serial >>> 14) & 0x03; + msg.messageCode = (msg.serial >>> 4) & 0x03FF; + msg.updateNumber = msg.serial & 0x0F; + }, + + /** + * Read GSM CBS message message identifier. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.1.2.2 + */ + readCbMessageIdentifier: function(msg) { + let Buf = this.context.Buf; + msg.messageId = Buf.readUint8() << 8 | Buf.readUint8(); + }, + + /** + * Read ETWS information from message identifier and serial Number + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.1.2.1 & 9.4.1.2.2 + */ + readCbEtwsInfo: function(msg) { + if ((msg.format != CB_FORMAT_ETWS) + && (msg.messageId >= CB_GSM_MESSAGEID_ETWS_BEGIN) + && (msg.messageId <= CB_GSM_MESSAGEID_ETWS_END)) { + // `In the case of transmitting CBS message for ETWS, a part of + // Message Code can be used to command mobile terminals to activate + // emergency user alert and message popup in order to alert the users.` + msg.etws = { + emergencyUserAlert: msg.messageCode & 0x0200 ? true : false, + popup: msg.messageCode & 0x0100 ? true : false + }; + + let warningType = msg.messageId - CB_GSM_MESSAGEID_ETWS_BEGIN; + if (warningType < CB_ETWS_WARNING_TYPE_NAMES.length) { + msg.etws.warningType = warningType; + } + } + }, + + /** + * Read CBS Data Coding Scheme. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.038 section 5. + */ + readCbDataCodingScheme: function(msg) { + let dcs = this.context.Buf.readUint8(); + if (DEBUG) this.context.debug("PDU: read CBS dcs: " + dcs); + + let language = null, hasLanguageIndicator = false; + // `Any reserved codings shall be assumed to be the GSM 7bit default + // alphabet.` + let encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; + let messageClass = PDU_DCS_MSG_CLASS_NORMAL; + + switch (dcs & PDU_DCS_CODING_GROUP_BITS) { + case 0x00: // 0000 + language = CB_DCS_LANG_GROUP_1[dcs & 0x0F]; + break; + + case 0x10: // 0001 + switch (dcs & 0x0F) { + case 0x00: + hasLanguageIndicator = true; + break; + case 0x01: + encoding = PDU_DCS_MSG_CODING_16BITS_ALPHABET; + hasLanguageIndicator = true; + break; + } + break; + + case 0x20: // 0010 + language = CB_DCS_LANG_GROUP_2[dcs & 0x0F]; + break; + + case 0x40: // 01xx + case 0x50: + //case 0x60: Text Compression, not supported + //case 0x70: Text Compression, not supported + case 0x90: // 1001 + encoding = (dcs & 0x0C); + if (encoding == 0x0C) { + encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; + } + messageClass = (dcs & PDU_DCS_MSG_CLASS_BITS); + break; + + case 0xF0: + encoding = (dcs & 0x04) ? PDU_DCS_MSG_CODING_8BITS_ALPHABET + : PDU_DCS_MSG_CODING_7BITS_ALPHABET; + switch(dcs & PDU_DCS_MSG_CLASS_BITS) { + case 0x01: messageClass = PDU_DCS_MSG_CLASS_USER_1; break; + case 0x02: messageClass = PDU_DCS_MSG_CLASS_USER_2; break; + case 0x03: messageClass = PDU_DCS_MSG_CLASS_3; break; + } + break; + + case 0x30: // 0011 (Reserved) + case 0x80: // 1000 (Reserved) + case 0xA0: // 1010..1100 (Reserved) + case 0xB0: + case 0xC0: + break; + + default: + throw new Error("Unsupported CBS data coding scheme: " + dcs); + } + + msg.dcs = dcs; + msg.encoding = encoding; + msg.language = language; + msg.messageClass = GECKO_SMS_MESSAGE_CLASSES[messageClass]; + msg.hasLanguageIndicator = hasLanguageIndicator; + }, + + /** + * Read GSM CBS message page parameter. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.1.2.4 + */ + readCbPageParameter: function(msg) { + let octet = this.context.Buf.readUint8(); + msg.pageIndex = (octet >>> 4) & 0x0F; + msg.numPages = octet & 0x0F; + if (!msg.pageIndex || !msg.numPages) { + // `If a mobile receives the code 0000 in either the first field or the + // second field then it shall treat the CBS message exactly the same as a + // CBS message with page parameter 0001 0001 (i.e. a single page message).` + msg.pageIndex = msg.numPages = 1; + } + }, + + /** + * Read ETWS Primary Notification message warning type. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.3.24 + */ + readCbWarningType: function(msg) { + let Buf = this.context.Buf; + let word = Buf.readUint8() << 8 | Buf.readUint8(); + msg.etws = { + warningType: (word >>> 9) & 0x7F, + popup: word & 0x80 ? true : false, + emergencyUserAlert: word & 0x100 ? true : false + }; + }, + + /** + * Read GSM CB Data + * + * This parameter is a copy of the 'CBS-Message-Information-Page' as sent + * from the CBC to the BSC. + * + * @see 3GPP TS 23.041 section 9.4.1.2.5 + */ + readGsmCbData: function(msg, length) { + let Buf = this.context.Buf; + let bufAdapter = { + context: this.context, + readHexOctet: function() { + return Buf.readUint8(); + } + }; + + msg.body = null; + msg.data = null; + switch (msg.encoding) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + msg.body = this.readSeptetsToString.call(bufAdapter, + Math.floor(length * 8 / 7), 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + if (msg.hasLanguageIndicator) { + msg.language = msg.body.substring(0, 2); + msg.body = msg.body.substring(3); + } + break; + + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + msg.data = Buf.readUint8Array(length); + break; + + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + if (msg.hasLanguageIndicator) { + msg.language = this.readSeptetsToString.call(bufAdapter, 2, 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + length -= 2; + } + msg.body = this.readUCS2String.call(bufAdapter, length); + break; + } + + if (msg.data || !msg.body) { + return; + } + + // According to 9.3.19 CBS-Message-Information-Page in TS 23.041: + // " + // This parameter is of a fixed length of 82 octets and carries up to and + // including 82 octets of user information. Where the user information is + // less than 82 octets, the remaining octets must be filled with padding. + // " + // According to 6.2.1.1 GSM 7 bit Default Alphabet and 6.2.3 UCS2 in + // TS 23.038, the padding character is <CR>. + for (let i = msg.body.length - 1; i >= 0; i--) { + if (msg.body.charAt(i) !== '\r') { + msg.body = msg.body.substring(0, i + 1); + break; + } + } + }, + + /** + * Read UMTS CB Data + * + * Octet Number(s) Parameter + * 1 Number-of-Pages + * 2 - 83 CBS-Message-Information-Page 1 + * 84 CBS-Message-Information-Length 1 + * ... + * CBS-Message-Information-Page n + * CBS-Message-Information-Length n + * + * @see 3GPP TS 23.041 section 9.4.2.2.5 + */ + readUmtsCbData: function(msg) { + let Buf = this.context.Buf; + let numOfPages = Buf.readUint8(); + if (numOfPages < 0 || numOfPages > 15) { + throw new Error("Invalid numOfPages: " + numOfPages); + } + + let bufAdapter = { + context: this.context, + readHexOctet: function() { + return Buf.readUint8(); + } + }; + + let removePaddingCharactors = function (text) { + for (let i = text.length - 1; i >= 0; i--) { + if (text.charAt(i) !== '\r') { + return text.substring(0, i + 1); + } + } + return text; + }; + + let totalLength = 0, length, pageLengths = []; + for (let i = 0; i < numOfPages; i++) { + Buf.seekIncoming(CB_MSG_PAGE_INFO_SIZE); + length = Buf.readUint8(); + totalLength += length; + pageLengths.push(length); + } + + // Seek back to beginning of CB Data. + Buf.seekIncoming(-numOfPages * (CB_MSG_PAGE_INFO_SIZE + 1)); + + switch (msg.encoding) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: { + let body; + msg.body = ""; + for (let i = 0; i < numOfPages; i++) { + body = this.readSeptetsToString.call(bufAdapter, + Math.floor(pageLengths[i] * 8 / 7), + 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + if (msg.hasLanguageIndicator) { + if (!msg.language) { + msg.language = body.substring(0, 2); + } + body = body.substring(3); + } + + msg.body += removePaddingCharactors(body); + + // Skip padding octets + Buf.seekIncoming(CB_MSG_PAGE_INFO_SIZE - pageLengths[i]); + // Read the octet of CBS-Message-Information-Length + Buf.readUint8(); + } + + break; + } + + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: { + msg.data = new Uint8Array(totalLength); + for (let i = 0, j = 0; i < numOfPages; i++) { + for (let pageLength = pageLengths[i]; pageLength > 0; pageLength--) { + msg.data[j++] = Buf.readUint8(); + } + + // Skip padding octets + Buf.seekIncoming(CB_MSG_PAGE_INFO_SIZE - pageLengths[i]); + // Read the octet of CBS-Message-Information-Length + Buf.readUint8(); + } + + break; + } + + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: { + msg.body = ""; + for (let i = 0; i < numOfPages; i++) { + let pageLength = pageLengths[i]; + if (msg.hasLanguageIndicator) { + if (!msg.language) { + msg.language = this.readSeptetsToString.call(bufAdapter, + 2, + 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + } else { + Buf.readUint16(); + } + + pageLength -= 2; + } + + msg.body += removePaddingCharactors( + this.readUCS2String.call(bufAdapter, pageLength)); + + // Skip padding octets + Buf.seekIncoming(CB_MSG_PAGE_INFO_SIZE - pageLengths[i]); + // Read the octet of CBS-Message-Information-Length + Buf.readUint8(); + } + + break; + } + } + }, + + /** + * Read Cell GSM/ETWS/UMTS Broadcast Message. + * + * @param pduLength + * total length of the incoming PDU in octets. + */ + readCbMessage: function(pduLength) { + // Validity GSM ETWS UMTS + let msg = { + // Internally used in ril_worker: + serial: null, // O O O + updateNumber: null, // O O O + format: null, // O O O + dcs: 0x0F, // O X O + encoding: PDU_DCS_MSG_CODING_7BITS_ALPHABET, // O X O + hasLanguageIndicator: false, // O X O + data: null, // O X O + body: null, // O X O + pageIndex: 1, // O X X + numPages: 1, // O X X + + // DOM attributes: + geographicalScope: null, // O O O + messageCode: null, // O O O + messageId: null, // O O O + language: null, // O X O + fullBody: null, // O X O + fullData: null, // O X O + messageClass: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], // O x O + etws: null // ? O ? + /*{ + warningType: null, // X O X + popup: false, // X O X + emergencyUserAlert: false, // X O X + }*/ + }; + + if (pduLength <= CB_MESSAGE_SIZE_ETWS) { + msg.format = CB_FORMAT_ETWS; + return this.readEtwsCbMessage(msg); + } + + if (pduLength <= CB_MESSAGE_SIZE_GSM) { + msg.format = CB_FORMAT_GSM; + return this.readGsmCbMessage(msg, pduLength); + } + + if (pduLength >= CB_MESSAGE_SIZE_UMTS_MIN && + pduLength <= CB_MESSAGE_SIZE_UMTS_MAX) { + msg.format = CB_FORMAT_UMTS; + return this.readUmtsCbMessage(msg); + } + + throw new Error("Invalid PDU Length: " + pduLength); + }, + + /** + * Read UMTS CBS Message. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 section 9.4.2 + * @see 3GPP TS 25.324 section 10.2 + */ + readUmtsCbMessage: function(msg) { + let Buf = this.context.Buf; + let type = Buf.readUint8(); + if (type != CB_UMTS_MESSAGE_TYPE_CBS) { + throw new Error("Unsupported UMTS Cell Broadcast message type: " + type); + } + + this.readCbMessageIdentifier(msg); + this.readCbSerialNumber(msg); + this.readCbEtwsInfo(msg); + this.readCbDataCodingScheme(msg); + this.readUmtsCbData(msg); + + return msg; + }, + + /** + * Read GSM Cell Broadcast Message. + * + * @param msg + * message object for output. + * @param pduLength + * total length of the incomint PDU in octets. + * + * @see 3GPP TS 23.041 clause 9.4.1.2 + */ + readGsmCbMessage: function(msg, pduLength) { + this.readCbSerialNumber(msg); + this.readCbMessageIdentifier(msg); + this.readCbEtwsInfo(msg); + this.readCbDataCodingScheme(msg); + this.readCbPageParameter(msg); + + // GSM CB message header takes 6 octets. + this.readGsmCbData(msg, pduLength - 6); + + return msg; + }, + + /** + * Read ETWS Primary Notification Message. + * + * @param msg + * message object for output. + * + * @see 3GPP TS 23.041 clause 9.4.1.3 + */ + readEtwsCbMessage: function(msg) { + this.readCbSerialNumber(msg); + this.readCbMessageIdentifier(msg); + this.readCbWarningType(msg); + + // Octet 7..56 is Warning Security Information. However, according to + // section 9.4.1.3.6, `The UE shall ignore this parameter.` So we just skip + // processing it here. + + return msg; + }, + + /** + * Read network name. + * + * @param len Length of the information element. + * @return + * { + * networkName: network name. + * shouldIncludeCi: Should Country's initials included in text string. + * } + * @see TS 24.008 clause 10.5.3.5a. + */ + readNetworkName: function(len) { + // According to TS 24.008 Sec. 10.5.3.5a, the first octet is: + // bit 8: must be 1. + // bit 5-7: Text encoding. + // 000 - GSM default alphabet. + // 001 - UCS2 (16 bit). + // else - reserved. + // bit 4: MS should add the letters for Country's Initials and a space + // to the text string if this bit is true. + // bit 1-3: number of spare bits in last octet. + + let codingInfo = this.readHexOctet(); + if (!(codingInfo & 0x80)) { + return null; + } + + let textEncoding = (codingInfo & 0x70) >> 4; + let shouldIncludeCountryInitials = !!(codingInfo & 0x08); + let spareBits = codingInfo & 0x07; + let resultString; + + switch (textEncoding) { + case 0: + // GSM Default alphabet. + resultString = this.readSeptetsToString( + Math.floor(((len - 1) * 8 - spareBits) / 7), 0, + PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + break; + case 1: + // UCS2 encoded. + resultString = this.context.ICCPDUHelper.readAlphaIdentifier(len - 1); + break; + default: + // Not an available text coding. + return null; + } + + // TODO - Bug 820286: According to shouldIncludeCountryInitials, add + // country initials to the resulting string. + return resultString; + } +}; + +/** + * Provide buffer with bitwise read/write function so make encoding/decoding easier. + */ +function BitBufferHelperObject(/* unused */aContext) { + this.readBuffer = []; + this.writeBuffer = []; +} +BitBufferHelperObject.prototype = { + readCache: 0, + readCacheSize: 0, + readBuffer: null, + readIndex: 0, + writeCache: 0, + writeCacheSize: 0, + writeBuffer: null, + + // Max length is 32 because we use integer as read/write cache. + // All read/write functions are implemented based on bitwise operation. + readBits: function(length) { + if (length <= 0 || length > 32) { + return null; + } + + if (length > this.readCacheSize) { + let bytesToRead = Math.ceil((length - this.readCacheSize) / 8); + for(let i = 0; i < bytesToRead; i++) { + this.readCache = (this.readCache << 8) | (this.readBuffer[this.readIndex++] & 0xFF); + this.readCacheSize += 8; + } + } + + let bitOffset = (this.readCacheSize - length), + resultMask = (1 << length) - 1, + result = 0; + + result = (this.readCache >> bitOffset) & resultMask; + this.readCacheSize -= length; + + return result; + }, + + backwardReadPilot: function(length) { + if (length <= 0) { + return; + } + + // Zero-based position. + let bitIndexToRead = this.readIndex * 8 - this.readCacheSize - length; + + if (bitIndexToRead < 0) { + return; + } + + // Update readIndex, readCache, readCacheSize accordingly. + let readBits = bitIndexToRead % 8; + this.readIndex = Math.floor(bitIndexToRead / 8) + ((readBits) ? 1 : 0); + this.readCache = (readBits) ? this.readBuffer[this.readIndex - 1] : 0; + this.readCacheSize = (readBits) ? (8 - readBits) : 0; + }, + + writeBits: function(value, length) { + if (length <= 0 || length > 32) { + return; + } + + let totalLength = length + this.writeCacheSize; + + // 8-byte cache not full + if (totalLength < 8) { + let valueMask = (1 << length) - 1; + this.writeCache = (this.writeCache << length) | (value & valueMask); + this.writeCacheSize += length; + return; + } + + // Deal with unaligned part + if (this.writeCacheSize) { + let mergeLength = 8 - this.writeCacheSize, + valueMask = (1 << mergeLength) - 1; + + this.writeCache = (this.writeCache << mergeLength) | ((value >> (length - mergeLength)) & valueMask); + this.writeBuffer.push(this.writeCache & 0xFF); + length -= mergeLength; + } + + // Aligned part, just copy + while (length >= 8) { + length -= 8; + this.writeBuffer.push((value >> length) & 0xFF); + } + + // Rest part is saved into cache + this.writeCacheSize = length; + this.writeCache = value & ((1 << length) - 1); + + return; + }, + + // Drop what still in read cache and goto next 8-byte alignment. + // There might be a better naming. + nextOctetAlign: function() { + this.readCache = 0; + this.readCacheSize = 0; + }, + + // Flush current write cache to Buf with padding 0s. + // There might be a better naming. + flushWithPadding: function() { + if (this.writeCacheSize) { + this.writeBuffer.push(this.writeCache << (8 - this.writeCacheSize)); + } + this.writeCache = 0; + this.writeCacheSize = 0; + }, + + startWrite: function(dataBuffer) { + this.writeBuffer = dataBuffer; + this.writeCache = 0; + this.writeCacheSize = 0; + }, + + startRead: function(dataBuffer) { + this.readBuffer = dataBuffer; + this.readCache = 0; + this.readCacheSize = 0; + this.readIndex = 0; + }, + + getWriteBufferSize: function() { + return this.writeBuffer.length; + }, + + overwriteWriteBuffer: function(position, data) { + let writeLength = data.length; + if (writeLength + position >= this.writeBuffer.length) { + writeLength = this.writeBuffer.length - position; + } + for (let i = 0; i < writeLength; i++) { + this.writeBuffer[i + position] = data[i]; + } + } +}; + +/** + * Helper for CDMA PDU + * + * Currently, some function are shared with GsmPDUHelper, they should be + * moved from GsmPDUHelper to a common object shared among GsmPDUHelper and + * CdmaPDUHelper. + */ +function CdmaPDUHelperObject(aContext) { + this.context = aContext; +} +CdmaPDUHelperObject.prototype = { + context: null, + + // 1..........C + // Only "1234567890*#" is defined in C.S0005-D v2.0 + dtmfChars: ".1234567890*#...", + + /** + * Entry point for SMS encoding, the options object is made compatible + * with existing writeMessage() of GsmPDUHelper, but less key is used. + * + * Current used key in options: + * @param number + * String containing the address (number) of the SMS receiver + * @param body + * String containing the message to be sent, segmented part + * @param dcs + * Data coding scheme. One of the PDU_DCS_MSG_CODING_*BITS_ALPHABET + * constants. + * @param encodedBodyLength + * Length of the user data when encoded with the given DCS. For UCS2, + * in bytes; for 7-bit, in septets. + * @param requestStatusReport + * Request status report. + * @param segmentRef + * Reference number of concatenated SMS message + * @param segmentMaxSeq + * Total number of concatenated SMS message + * @param segmentSeq + * Sequence number of concatenated SMS message + */ + writeMessage: function(options) { + if (DEBUG) { + this.context.debug("cdma_writeMessage: " + JSON.stringify(options)); + } + + // Get encoding + options.encoding = this.gsmDcsToCdmaEncoding(options.dcs); + + // Common Header + if (options.segmentMaxSeq > 1) { + this.writeInt(PDU_CDMA_MSG_TELESERIVCIE_ID_WEMT); + } else { + this.writeInt(PDU_CDMA_MSG_TELESERIVCIE_ID_SMS); + } + + this.writeInt(0); + this.writeInt(PDU_CDMA_MSG_CATEGORY_UNSPEC); + + // Just fill out address info in byte, rild will encap them for us + let addrInfo = this.encodeAddr(options.number); + this.writeByte(addrInfo.digitMode); + this.writeByte(addrInfo.numberMode); + this.writeByte(addrInfo.numberType); + this.writeByte(addrInfo.numberPlan); + this.writeByte(addrInfo.address.length); + for (let i = 0; i < addrInfo.address.length; i++) { + this.writeByte(addrInfo.address[i]); + } + + // Subaddress, not supported + this.writeByte(0); // Subaddress : Type + this.writeByte(0); // Subaddress : Odd + this.writeByte(0); // Subaddress : length + + // User Data + let encodeResult = this.encodeUserData(options); + this.writeByte(encodeResult.length); + for (let i = 0; i < encodeResult.length; i++) { + this.writeByte(encodeResult[i]); + } + + encodeResult = null; + }, + + /** + * Data writters + */ + writeInt: function(value) { + this.context.Buf.writeInt32(value); + }, + + writeByte: function(value) { + this.context.Buf.writeInt32(value & 0xFF); + }, + + /** + * Transform GSM DCS to CDMA encoding. + */ + gsmDcsToCdmaEncoding: function(encoding) { + switch (encoding) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + return PDU_CDMA_MSG_CODING_7BITS_ASCII; + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + return PDU_CDMA_MSG_CODING_OCTET; + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + return PDU_CDMA_MSG_CODING_UNICODE; + } + throw new Error("gsmDcsToCdmaEncoding(): Invalid GSM SMS DCS value: " + encoding); + }, + + /** + * Encode address into CDMA address format, as a byte array. + * + * @see 3GGP2 C.S0015-B 2.0, 3.4.3.3 Address Parameters + * + * @param address + * String of address to be encoded + */ + encodeAddr: function(address) { + let result = {}; + + result.numberType = PDU_CDMA_MSG_ADDR_NUMBER_TYPE_UNKNOWN; + result.numberPlan = PDU_CDMA_MSG_ADDR_NUMBER_TYPE_UNKNOWN; + + if (address[0] === '+') { + address = address.substring(1); + } + + // Try encode with DTMF first + result.digitMode = PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF; + result.numberMode = PDU_CDMA_MSG_ADDR_NUMBER_MODE_ANSI; + + result.address = []; + for (let i = 0; i < address.length; i++) { + let addrDigit = this.dtmfChars.indexOf(address.charAt(i)); + if (addrDigit < 0) { + result.digitMode = PDU_CDMA_MSG_ADDR_DIGIT_MODE_ASCII; + result.numberMode = PDU_CDMA_MSG_ADDR_NUMBER_MODE_ASCII; + result.address = []; + break; + } + result.address.push(addrDigit); + } + + // Address can't be encoded with DTMF, then use 7-bit ASCII + if (result.digitMode !== PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF) { + if (address.indexOf("@") !== -1) { + result.numberType = PDU_CDMA_MSG_ADDR_NUMBER_TYPE_NATIONAL; + } + + for (let i = 0; i < address.length; i++) { + result.address.push(address.charCodeAt(i) & 0x7F); + } + } + + return result; + }, + + /** + * Encode SMS contents in options into CDMA userData field. + * Corresponding and required subparameters will be added automatically. + * + * @see 3GGP2 C.S0015-B 2.0, 3.4.3.7 Bearer Data + * 4.5 Bearer Data Parameters + * + * Current used key in options: + * @param body + * String containing the message to be sent, segmented part + * @param encoding + * Encoding method of CDMA, can be transformed from GSM DCS by function + * cdmaPduHelp.gsmDcsToCdmaEncoding() + * @param encodedBodyLength + * Length of the user data when encoded with the given DCS. For UCS2, + * in bytes; for 7-bit, in septets. + * @param requestStatusReport + * Request status report. + * @param segmentRef + * Reference number of concatenated SMS message + * @param segmentMaxSeq + * Total number of concatenated SMS message + * @param segmentSeq + * Sequence number of concatenated SMS message + */ + encodeUserData: function(options) { + let userDataBuffer = []; + this.context.BitBufferHelper.startWrite(userDataBuffer); + + // Message Identifier + this.encodeUserDataMsgId(options); + + // User Data + this.encodeUserDataMsg(options); + + // Reply Option + this.encodeUserDataReplyOption(options); + + return userDataBuffer; + }, + + /** + * User data subparameter encoder : Message Identifier + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.1 Message Identifier + */ + encodeUserDataMsgId: function(options) { + let BitBufferHelper = this.context.BitBufferHelper; + BitBufferHelper.writeBits(PDU_CDMA_MSG_USERDATA_MSG_ID, 8); + BitBufferHelper.writeBits(3, 8); + BitBufferHelper.writeBits(PDU_CDMA_MSG_TYPE_SUBMIT, 4); + BitBufferHelper.writeBits(1, 16); // TODO: How to get message ID? + if (options.segmentMaxSeq > 1) { + BitBufferHelper.writeBits(1, 1); + } else { + BitBufferHelper.writeBits(0, 1); + } + + BitBufferHelper.flushWithPadding(); + }, + + /** + * User data subparameter encoder : User Data + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.2 User Data + */ + encodeUserDataMsg: function(options) { + let BitBufferHelper = this.context.BitBufferHelper; + BitBufferHelper.writeBits(PDU_CDMA_MSG_USERDATA_BODY, 8); + // Reserve space for length + BitBufferHelper.writeBits(0, 8); + let lengthPosition = BitBufferHelper.getWriteBufferSize(); + + BitBufferHelper.writeBits(options.encoding, 5); + + // Add user data header for message segement + let msgBody = options.body, + msgBodySize = (options.encoding === PDU_CDMA_MSG_CODING_7BITS_ASCII ? + options.encodedBodyLength : + msgBody.length); + if (options.segmentMaxSeq > 1) { + if (options.encoding === PDU_CDMA_MSG_CODING_7BITS_ASCII) { + BitBufferHelper.writeBits(msgBodySize + 7, 8); // Required length for user data header, in septet(7-bit) + + BitBufferHelper.writeBits(5, 8); // total header length 5 bytes + BitBufferHelper.writeBits(PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT, 8); // header id 0 + BitBufferHelper.writeBits(3, 8); // length of element for id 0 is 3 + BitBufferHelper.writeBits(options.segmentRef & 0xFF, 8); // Segement reference + BitBufferHelper.writeBits(options.segmentMaxSeq & 0xFF, 8); // Max segment + BitBufferHelper.writeBits(options.segmentSeq & 0xFF, 8); // Current segment + BitBufferHelper.writeBits(0, 1); // Padding to make header data septet(7-bit) aligned + } else { + if (options.encoding === PDU_CDMA_MSG_CODING_UNICODE) { + BitBufferHelper.writeBits(msgBodySize + 3, 8); // Required length for user data header, in 16-bit + } else { + BitBufferHelper.writeBits(msgBodySize + 6, 8); // Required length for user data header, in octet(8-bit) + } + + BitBufferHelper.writeBits(5, 8); // total header length 5 bytes + BitBufferHelper.writeBits(PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT, 8); // header id 0 + BitBufferHelper.writeBits(3, 8); // length of element for id 0 is 3 + BitBufferHelper.writeBits(options.segmentRef & 0xFF, 8); // Segement reference + BitBufferHelper.writeBits(options.segmentMaxSeq & 0xFF, 8); // Max segment + BitBufferHelper.writeBits(options.segmentSeq & 0xFF, 8); // Current segment + } + } else { + BitBufferHelper.writeBits(msgBodySize, 8); + } + + // Encode message based on encoding method + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + for (let i = 0; i < msgBody.length; i++) { + switch (options.encoding) { + case PDU_CDMA_MSG_CODING_OCTET: { + let msgDigit = msgBody.charCodeAt(i); + BitBufferHelper.writeBits(msgDigit, 8); + break; + } + case PDU_CDMA_MSG_CODING_7BITS_ASCII: { + let msgDigit = msgBody.charCodeAt(i), + msgDigitChar = msgBody.charAt(i); + + if (msgDigit >= 32) { + BitBufferHelper.writeBits(msgDigit, 7); + } else { + msgDigit = langTable.indexOf(msgDigitChar); + + if (msgDigit === PDU_NL_EXTENDED_ESCAPE) { + break; + } + if (msgDigit >= 0) { + BitBufferHelper.writeBits(msgDigit, 7); + } else { + msgDigit = langShiftTable.indexOf(msgDigitChar); + if (msgDigit == -1) { + throw new Error("'" + msgDigitChar + "' is not in 7 bit alphabet " + + langIndex + ":" + langShiftIndex + "!"); + } + + if (msgDigit === PDU_NL_RESERVED_CONTROL) { + break; + } + + BitBufferHelper.writeBits(PDU_NL_EXTENDED_ESCAPE, 7); + BitBufferHelper.writeBits(msgDigit, 7); + } + } + break; + } + case PDU_CDMA_MSG_CODING_UNICODE: { + let msgDigit = msgBody.charCodeAt(i); + BitBufferHelper.writeBits(msgDigit, 16); + break; + } + } + } + BitBufferHelper.flushWithPadding(); + + // Fill length + let currentPosition = BitBufferHelper.getWriteBufferSize(); + BitBufferHelper.overwriteWriteBuffer(lengthPosition - 1, [currentPosition - lengthPosition]); + }, + + /** + * User data subparameter encoder : Reply Option + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.11 Reply Option + */ + encodeUserDataReplyOption: function(options) { + if (options.requestStatusReport) { + let BitBufferHelper = this.context.BitBufferHelper; + BitBufferHelper.writeBits(PDU_CDMA_MSG_USERDATA_REPLY_OPTION, 8); + BitBufferHelper.writeBits(1, 8); + BitBufferHelper.writeBits(0, 1); // USER_ACK_REQ + BitBufferHelper.writeBits(1, 1); // DAK_REQ + BitBufferHelper.flushWithPadding(); + } + }, + + /** + * Entry point for SMS decoding, the returned object is made compatible + * with existing readMessage() of GsmPDUHelper + */ + readMessage: function() { + let message = {}; + + // Teleservice Identifier + message.teleservice = this.readInt(); + + // Message Type + let isServicePresent = this.readByte(); + if (isServicePresent) { + message.messageType = PDU_CDMA_MSG_TYPE_BROADCAST; + } else { + if (message.teleservice) { + message.messageType = PDU_CDMA_MSG_TYPE_P2P; + } else { + message.messageType = PDU_CDMA_MSG_TYPE_ACK; + } + } + + // Service Category + message.service = this.readInt(); + + // Originated Address + let addrInfo = {}; + addrInfo.digitMode = (this.readInt() & 0x01); + addrInfo.numberMode = (this.readInt() & 0x01); + addrInfo.numberType = (this.readInt() & 0x01); + addrInfo.numberPlan = (this.readInt() & 0x01); + addrInfo.addrLength = this.readByte(); + addrInfo.address = []; + for (let i = 0; i < addrInfo.addrLength; i++) { + addrInfo.address.push(this.readByte()); + } + message.sender = this.decodeAddr(addrInfo); + + // Originated Subaddress + addrInfo.Type = (this.readInt() & 0x07); + addrInfo.Odd = (this.readByte() & 0x01); + addrInfo.addrLength = this.readByte(); + for (let i = 0; i < addrInfo.addrLength; i++) { + let addrDigit = this.readByte(); + message.sender += String.fromCharCode(addrDigit); + } + + // Bearer Data + this.decodeUserData(message); + + // Bearer Data Sub-Parameter: User Data + let userData = message[PDU_CDMA_MSG_USERDATA_BODY]; + [message.header, message.body, message.encoding, message.data] = + (userData) ? [userData.header, userData.body, userData.encoding, userData.data] + : [null, null, null, null]; + + // Bearer Data Sub-Parameter: Message Status + // Success Delivery (0) if both Message Status and User Data are absent. + // Message Status absent (-1) if only User Data is available. + let msgStatus = message[PDU_CDMA_MSG_USER_DATA_MSG_STATUS]; + [message.errorClass, message.msgStatus] = + (msgStatus) ? [msgStatus.errorClass, msgStatus.msgStatus] + : ((message.body) ? [-1, -1] : [0, 0]); + + // Transform message to GSM msg + let msg = { + SMSC: "", + mti: 0, + udhi: 0, + sender: message.sender, + recipient: null, + pid: PDU_PID_DEFAULT, + epid: PDU_PID_DEFAULT, + dcs: 0, + mwi: null, + replace: false, + header: message.header, + body: message.body, + data: message.data, + sentTimestamp: message[PDU_CDMA_MSG_USERDATA_TIMESTAMP], + language: message[PDU_CDMA_LANGUAGE_INDICATOR], + status: null, + scts: null, + dt: null, + encoding: message.encoding, + messageClass: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + messageType: message.messageType, + serviceCategory: message.service, + subMsgType: message[PDU_CDMA_MSG_USERDATA_MSG_ID].msgType, + msgId: message[PDU_CDMA_MSG_USERDATA_MSG_ID].msgId, + errorClass: message.errorClass, + msgStatus: message.msgStatus, + teleservice: message.teleservice + }; + + return msg; + }, + + /** + * Helper for processing received SMS parcel data. + * + * @param length + * Length of SMS string in the incoming parcel. + * + * @return Message parsed or null for invalid message. + */ + processReceivedSms: function(length) { + if (!length) { + if (DEBUG) this.context.debug("Received empty SMS!"); + return [null, PDU_FCS_UNSPECIFIED]; + } + + let message = this.readMessage(); + if (DEBUG) this.context.debug("Got new SMS: " + JSON.stringify(message)); + + // Determine result + if (!message) { + return [null, PDU_FCS_UNSPECIFIED]; + } + + return [message, PDU_FCS_OK]; + }, + + /** + * Data readers + */ + readInt: function() { + return this.context.Buf.readInt32(); + }, + + readByte: function() { + return (this.context.Buf.readInt32() & 0xFF); + }, + + /** + * Decode CDMA address data into address string + * + * @see 3GGP2 C.S0015-B 2.0, 3.4.3.3 Address Parameters + * + * Required key in addrInfo + * @param addrLength + * Length of address + * @param digitMode + * Address encoding method + * @param address + * Array of encoded address data + */ + decodeAddr: function(addrInfo) { + let result = ""; + for (let i = 0; i < addrInfo.addrLength; i++) { + if (addrInfo.digitMode === PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF) { + result += this.dtmfChars.charAt(addrInfo.address[i]); + } else { + result += String.fromCharCode(addrInfo.address[i]); + } + } + return result; + }, + + /** + * Read userData in parcel buffer and decode into message object. + * Each subparameter will be stored in corresponding key. + * + * @see 3GGP2 C.S0015-B 2.0, 3.4.3.7 Bearer Data + * 4.5 Bearer Data Parameters + */ + decodeUserData: function(message) { + let userDataLength = this.readInt(); + + while (userDataLength > 0) { + let id = this.readByte(), + length = this.readByte(), + userDataBuffer = []; + + for (let i = 0; i < length; i++) { + userDataBuffer.push(this.readByte()); + } + + this.context.BitBufferHelper.startRead(userDataBuffer); + + switch (id) { + case PDU_CDMA_MSG_USERDATA_MSG_ID: + message[id] = this.decodeUserDataMsgId(); + break; + case PDU_CDMA_MSG_USERDATA_BODY: + message[id] = this.decodeUserDataMsg(message[PDU_CDMA_MSG_USERDATA_MSG_ID].userHeader); + break; + case PDU_CDMA_MSG_USERDATA_TIMESTAMP: + message[id] = this.decodeUserDataTimestamp(); + break; + case PDU_CDMA_MSG_USERDATA_REPLY_OPTION: + message[id] = this.decodeUserDataReplyOption(); + break; + case PDU_CDMA_LANGUAGE_INDICATOR: + message[id] = this.decodeLanguageIndicator(); + break; + case PDU_CDMA_MSG_USERDATA_CALLBACK_NUMBER: + message[id] = this.decodeUserDataCallbackNumber(); + break; + case PDU_CDMA_MSG_USER_DATA_MSG_STATUS: + message[id] = this.decodeUserDataMsgStatus(); + break; + } + + userDataLength -= (length + 2); + userDataBuffer = []; + } + }, + + /** + * User data subparameter decoder: Message Identifier + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.1 Message Identifier + */ + decodeUserDataMsgId: function() { + let result = {}; + let BitBufferHelper = this.context.BitBufferHelper; + result.msgType = BitBufferHelper.readBits(4); + result.msgId = BitBufferHelper.readBits(16); + result.userHeader = BitBufferHelper.readBits(1); + + return result; + }, + + /** + * Decode user data header, we only care about segment information + * on CDMA. + * + * This function is mostly copied from gsmPduHelper.readUserDataHeader() but + * change the read function, because CDMA user header decoding is't byte-wise + * aligned. + */ + decodeUserDataHeader: function(encoding) { + let BitBufferHelper = this.context.BitBufferHelper; + let header = {}, + headerSize = BitBufferHelper.readBits(8), + userDataHeaderSize = headerSize + 1, + headerPaddingBits = 0; + + // Calculate header size + if (encoding === PDU_DCS_MSG_CODING_7BITS_ALPHABET) { + // Length is in 7-bit + header.length = Math.ceil(userDataHeaderSize * 8 / 7); + // Calulate padding length + headerPaddingBits = (header.length * 7) - (userDataHeaderSize * 8); + } else if (encoding === PDU_DCS_MSG_CODING_8BITS_ALPHABET) { + header.length = userDataHeaderSize; + } else { + header.length = userDataHeaderSize / 2; + } + + while (headerSize) { + let identifier = BitBufferHelper.readBits(8), + length = BitBufferHelper.readBits(8); + + headerSize -= (2 + length); + + switch (identifier) { + case PDU_IEI_CONCATENATED_SHORT_MESSAGES_8BIT: { + let ref = BitBufferHelper.readBits(8), + max = BitBufferHelper.readBits(8), + seq = BitBufferHelper.readBits(8); + if (max && seq && (seq <= max)) { + header.segmentRef = ref; + header.segmentMaxSeq = max; + header.segmentSeq = seq; + } + break; + } + case PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_8BIT: { + let dstp = BitBufferHelper.readBits(8), + orip = BitBufferHelper.readBits(8); + if ((dstp < PDU_APA_RESERVED_8BIT_PORTS) + || (orip < PDU_APA_RESERVED_8BIT_PORTS)) { + // 3GPP TS 23.040 clause 9.2.3.24.3: "A receiving entity shall + // ignore any information element where the value of the + // Information-Element-Data is Reserved or not supported" + break; + } + header.destinationPort = dstp; + header.originatorPort = orip; + break; + } + case PDU_IEI_APPLICATION_PORT_ADDRESSING_SCHEME_16BIT: { + let dstp = (BitBufferHelper.readBits(8) << 8) | BitBufferHelper.readBits(8), + orip = (BitBufferHelper.readBits(8) << 8) | BitBufferHelper.readBits(8); + // 3GPP TS 23.040 clause 9.2.3.24.4: "A receiving entity shall + // ignore any information element where the value of the + // Information-Element-Data is Reserved or not supported" + if ((dstp < PDU_APA_VALID_16BIT_PORTS) + && (orip < PDU_APA_VALID_16BIT_PORTS)) { + header.destinationPort = dstp; + header.originatorPort = orip; + } + break; + } + case PDU_IEI_CONCATENATED_SHORT_MESSAGES_16BIT: { + let ref = (BitBufferHelper.readBits(8) << 8) | BitBufferHelper.readBits(8), + max = BitBufferHelper.readBits(8), + seq = BitBufferHelper.readBits(8); + if (max && seq && (seq <= max)) { + header.segmentRef = ref; + header.segmentMaxSeq = max; + header.segmentSeq = seq; + } + break; + } + case PDU_IEI_NATIONAL_LANGUAGE_SINGLE_SHIFT: { + let langShiftIndex = BitBufferHelper.readBits(8); + if (langShiftIndex < PDU_NL_SINGLE_SHIFT_TABLES.length) { + header.langShiftIndex = langShiftIndex; + } + break; + } + case PDU_IEI_NATIONAL_LANGUAGE_LOCKING_SHIFT: { + let langIndex = BitBufferHelper.readBits(8); + if (langIndex < PDU_NL_LOCKING_SHIFT_TABLES.length) { + header.langIndex = langIndex; + } + break; + } + case PDU_IEI_SPECIAL_SMS_MESSAGE_INDICATION: { + let msgInd = BitBufferHelper.readBits(8) & 0xFF, + msgCount = BitBufferHelper.readBits(8); + /* + * TS 23.040 V6.8.1 Sec 9.2.3.24.2 + * bits 1 0 : basic message indication type + * bits 4 3 2 : extended message indication type + * bits 6 5 : Profile id + * bit 7 : storage type + */ + let storeType = msgInd & PDU_MWI_STORE_TYPE_BIT; + header.mwi = {}; + mwi = header.mwi; + + if (storeType == PDU_MWI_STORE_TYPE_STORE) { + // Store message because TP_UDH indicates so, note this may override + // the setting in DCS, but that is expected + mwi.discard = false; + } else if (mwi.discard === undefined) { + // storeType == PDU_MWI_STORE_TYPE_DISCARD + // only override mwi.discard here if it hasn't already been set + mwi.discard = true; + } + + mwi.msgCount = msgCount & 0xFF; + mwi.active = mwi.msgCount > 0; + + if (DEBUG) { + this.context.debug("MWI in TP_UDH received: " + JSON.stringify(mwi)); + } + break; + } + default: + // Drop unsupported id + for (let i = 0; i < length; i++) { + BitBufferHelper.readBits(8); + } + } + } + + // Consume padding bits + if (headerPaddingBits) { + BitBufferHelper.readBits(headerPaddingBits); + } + + return header; + }, + + getCdmaMsgEncoding: function(encoding) { + // Determine encoding method + switch (encoding) { + case PDU_CDMA_MSG_CODING_7BITS_ASCII: + case PDU_CDMA_MSG_CODING_IA5: + case PDU_CDMA_MSG_CODING_7BITS_GSM: + return PDU_DCS_MSG_CODING_7BITS_ALPHABET; + case PDU_CDMA_MSG_CODING_OCTET: + case PDU_CDMA_MSG_CODING_IS_91: + case PDU_CDMA_MSG_CODING_LATIN_HEBREW: + case PDU_CDMA_MSG_CODING_LATIN: + return PDU_DCS_MSG_CODING_8BITS_ALPHABET; + case PDU_CDMA_MSG_CODING_UNICODE: + case PDU_CDMA_MSG_CODING_SHIFT_JIS: + case PDU_CDMA_MSG_CODING_KOREAN: + return PDU_DCS_MSG_CODING_16BITS_ALPHABET; + } + return null; + }, + + decodeCdmaPDUMsg: function(encoding, msgType, msgBodySize) { + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + let BitBufferHelper = this.context.BitBufferHelper; + let result = ""; + let msgDigit; + switch (encoding) { + case PDU_CDMA_MSG_CODING_OCTET: // TODO : Require Test + while(msgBodySize > 0) { + msgDigit = String.fromCharCode(BitBufferHelper.readBits(8)); + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_IS_91: // TODO : Require Test + // Referenced from android code + switch (msgType) { + case PDU_CDMA_MSG_CODING_IS_91_TYPE_SMS: + case PDU_CDMA_MSG_CODING_IS_91_TYPE_SMS_FULL: + case PDU_CDMA_MSG_CODING_IS_91_TYPE_VOICEMAIL_STATUS: + while(msgBodySize > 0) { + msgDigit = String.fromCharCode(BitBufferHelper.readBits(6) + 0x20); + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_IS_91_TYPE_CLI: + let addrInfo = {}; + addrInfo.digitMode = PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF; + addrInfo.numberMode = PDU_CDMA_MSG_ADDR_NUMBER_MODE_ANSI; + addrInfo.numberType = PDU_CDMA_MSG_ADDR_NUMBER_TYPE_UNKNOWN; + addrInfo.numberPlan = PDU_CDMA_MSG_ADDR_NUMBER_PLAN_UNKNOWN; + addrInfo.addrLength = msgBodySize; + addrInfo.address = []; + for (let i = 0; i < addrInfo.addrLength; i++) { + addrInfo.address.push(BitBufferHelper.readBits(4)); + } + result = this.decodeAddr(addrInfo); + break; + } + // Fall through. + case PDU_CDMA_MSG_CODING_7BITS_ASCII: + case PDU_CDMA_MSG_CODING_IA5: // TODO : Require Test + while(msgBodySize > 0) { + msgDigit = BitBufferHelper.readBits(7); + if (msgDigit >= 32) { + msgDigit = String.fromCharCode(msgDigit); + } else { + if (msgDigit !== PDU_NL_EXTENDED_ESCAPE) { + msgDigit = langTable[msgDigit]; + } else { + msgDigit = BitBufferHelper.readBits(7); + msgBodySize--; + msgDigit = langShiftTable[msgDigit]; + } + } + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_UNICODE: + while(msgBodySize > 0) { + msgDigit = String.fromCharCode(BitBufferHelper.readBits(16)); + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_7BITS_GSM: // TODO : Require Test + while(msgBodySize > 0) { + msgDigit = BitBufferHelper.readBits(7); + if (msgDigit !== PDU_NL_EXTENDED_ESCAPE) { + msgDigit = langTable[msgDigit]; + } else { + msgDigit = BitBufferHelper.readBits(7); + msgBodySize--; + msgDigit = langShiftTable[msgDigit]; + } + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_LATIN: // TODO : Require Test + // Reference : http://en.wikipedia.org/wiki/ISO/IEC_8859-1 + while(msgBodySize > 0) { + msgDigit = String.fromCharCode(BitBufferHelper.readBits(8)); + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_LATIN_HEBREW: // TODO : Require Test + // Reference : http://en.wikipedia.org/wiki/ISO/IEC_8859-8 + while(msgBodySize > 0) { + msgDigit = BitBufferHelper.readBits(8); + if (msgDigit === 0xDF) { + msgDigit = String.fromCharCode(0x2017); + } else if (msgDigit === 0xFD) { + msgDigit = String.fromCharCode(0x200E); + } else if (msgDigit === 0xFE) { + msgDigit = String.fromCharCode(0x200F); + } else if (msgDigit >= 0xE0 && msgDigit <= 0xFA) { + msgDigit = String.fromCharCode(0x4F0 + msgDigit); + } else { + msgDigit = String.fromCharCode(msgDigit); + } + result += msgDigit; + msgBodySize--; + } + break; + case PDU_CDMA_MSG_CODING_SHIFT_JIS: + // Reference : http://msdn.microsoft.com/en-US/goglobal/cc305152.aspx + // http://demo.icu-project.org/icu-bin/convexp?conv=Shift_JIS + let shift_jis_message = []; + + while (msgBodySize > 0) { + shift_jis_message.push(BitBufferHelper.readBits(8)); + msgBodySize--; + } + + let decoder = new TextDecoder("shift_jis"); + result = decoder.decode(new Uint8Array(shift_jis_message)); + break; + case PDU_CDMA_MSG_CODING_KOREAN: + case PDU_CDMA_MSG_CODING_GSM_DCS: + // Fall through. + default: + break; + } + return result; + }, + + /** + * User data subparameter decoder : User Data + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.2 User Data + */ + decodeUserDataMsg: function(hasUserHeader) { + let BitBufferHelper = this.context.BitBufferHelper; + let result = {}, + encoding = BitBufferHelper.readBits(5), + msgType; + + if (encoding === PDU_CDMA_MSG_CODING_IS_91) { + msgType = BitBufferHelper.readBits(8); + } + result.encoding = this.getCdmaMsgEncoding(encoding); + + let msgBodySize = BitBufferHelper.readBits(8); + + // For segmented SMS, a user header is included before sms content + if (hasUserHeader) { + result.header = this.decodeUserDataHeader(result.encoding); + // header size is included in body size, they are decoded + msgBodySize -= result.header.length; + } + + // Store original payload if enconding is OCTET for further handling of WAP Push, etc. + if (encoding === PDU_CDMA_MSG_CODING_OCTET && msgBodySize > 0) { + result.data = new Uint8Array(msgBodySize); + for (let i = 0; i < msgBodySize; i++) { + result.data[i] = BitBufferHelper.readBits(8); + } + BitBufferHelper.backwardReadPilot(8 * msgBodySize); + } + + // Decode sms content + result.body = this.decodeCdmaPDUMsg(encoding, msgType, msgBodySize); + + return result; + }, + + decodeBcd: function(value) { + return ((value >> 4) & 0xF) * 10 + (value & 0x0F); + }, + + /** + * User data subparameter decoder : Time Stamp + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.4 Message Center Time Stamp + */ + decodeUserDataTimestamp: function() { + let BitBufferHelper = this.context.BitBufferHelper; + let year = this.decodeBcd(BitBufferHelper.readBits(8)), + month = this.decodeBcd(BitBufferHelper.readBits(8)) - 1, + date = this.decodeBcd(BitBufferHelper.readBits(8)), + hour = this.decodeBcd(BitBufferHelper.readBits(8)), + min = this.decodeBcd(BitBufferHelper.readBits(8)), + sec = this.decodeBcd(BitBufferHelper.readBits(8)); + + if (year >= 96 && year <= 99) { + year += 1900; + } else { + year += 2000; + } + + let result = (new Date(year, month, date, hour, min, sec, 0)).valueOf(); + + return result; + }, + + /** + * User data subparameter decoder : Reply Option + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.11 Reply Option + */ + decodeUserDataReplyOption: function() { + let replyAction = this.context.BitBufferHelper.readBits(4), + result = { userAck: (replyAction & 0x8) ? true : false, + deliverAck: (replyAction & 0x4) ? true : false, + readAck: (replyAction & 0x2) ? true : false, + report: (replyAction & 0x1) ? true : false + }; + + return result; + }, + + /** + * User data subparameter decoder : Language Indicator + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.14 Language Indicator + */ + decodeLanguageIndicator: function() { + let language = this.context.BitBufferHelper.readBits(8); + let result = CB_CDMA_LANG_GROUP[language]; + return result; + }, + + /** + * User data subparameter decoder : Call-Back Number + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.15 Call-Back Number + */ + decodeUserDataCallbackNumber: function() { + let BitBufferHelper = this.context.BitBufferHelper; + let digitMode = BitBufferHelper.readBits(1); + if (digitMode) { + let numberType = BitBufferHelper.readBits(3), + numberPlan = BitBufferHelper.readBits(4); + } + let numberFields = BitBufferHelper.readBits(8), + result = ""; + for (let i = 0; i < numberFields; i++) { + if (digitMode === PDU_CDMA_MSG_ADDR_DIGIT_MODE_DTMF) { + let addrDigit = BitBufferHelper.readBits(4); + result += this.dtmfChars.charAt(addrDigit); + } else { + let addrDigit = BitBufferHelper.readBits(8); + result += String.fromCharCode(addrDigit); + } + } + + return result; + }, + + /** + * User data subparameter decoder : Message Status + * + * @see 3GGP2 C.S0015-B 2.0, 4.5.21 Message Status + */ + decodeUserDataMsgStatus: function() { + let BitBufferHelper = this.context.BitBufferHelper; + let result = { + errorClass: BitBufferHelper.readBits(2), + msgStatus: BitBufferHelper.readBits(6) + }; + + return result; + }, + + /** + * Decode information record parcel. + */ + decodeInformationRecord: function() { + let Buf = this.context.Buf; + let records = []; + let numOfRecords = Buf.readInt32(); + + let type; + let record; + for (let i = 0; i < numOfRecords; i++) { + record = {}; + type = Buf.readInt32(); + + switch (type) { + /* + * Every type is encaped by ril, except extended display + */ + case PDU_CDMA_INFO_REC_TYPE_DISPLAY: + case PDU_CDMA_INFO_REC_TYPE_EXTENDED_DISPLAY: + record.display = Buf.readString(); + break; + case PDU_CDMA_INFO_REC_TYPE_CALLED_PARTY_NUMBER: + record.calledNumber = {}; + record.calledNumber.number = Buf.readString(); + record.calledNumber.type = Buf.readInt32(); + record.calledNumber.plan = Buf.readInt32(); + record.calledNumber.pi = Buf.readInt32(); + record.calledNumber.si = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_CALLING_PARTY_NUMBER: + record.callingNumber = {}; + record.callingNumber.number = Buf.readString(); + record.callingNumber.type = Buf.readInt32(); + record.callingNumber.plan = Buf.readInt32(); + record.callingNumber.pi = Buf.readInt32(); + record.callingNumber.si = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_CONNECTED_NUMBER: + record.connectedNumber = {}; + record.connectedNumber.number = Buf.readString(); + record.connectedNumber.type = Buf.readInt32(); + record.connectedNumber.plan = Buf.readInt32(); + record.connectedNumber.pi = Buf.readInt32(); + record.connectedNumber.si = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_SIGNAL: + record.signal = {}; + if (!Buf.readInt32()) { // Non-zero if signal is present. + Buf.seekIncoming(3 * Buf.UINT32_SIZE); + continue; + } + record.signal.type = Buf.readInt32(); + record.signal.alertPitch = Buf.readInt32(); + record.signal.signal = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_REDIRECTING_NUMBER: + record.redirect = {}; + record.redirect.number = Buf.readString(); + record.redirect.type = Buf.readInt32(); + record.redirect.plan = Buf.readInt32(); + record.redirect.pi = Buf.readInt32(); + record.redirect.si = Buf.readInt32(); + record.redirect.reason = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_LINE_CONTROL: + record.lineControl = {}; + record.lineControl.polarityIncluded = Buf.readInt32(); + record.lineControl.toggle = Buf.readInt32(); + record.lineControl.reverse = Buf.readInt32(); + record.lineControl.powerDenial = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_T53_CLIR: + record.clirCause = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_T53_AUDIO_CONTROL: + record.audioControl = {}; + record.audioControl.upLink = Buf.readInt32(); + record.audioControl.downLink = Buf.readInt32(); + break; + case PDU_CDMA_INFO_REC_TYPE_T53_RELEASE: + // Fall through + default: + throw new Error("UNSOLICITED_CDMA_INFO_REC(), Unsupported information record type " + type + "\n"); + } + + records.push(record); + } + + return records; + } +}; + +/** + * Helper for processing ICC PDUs. + */ +function ICCPDUHelperObject(aContext) { + this.context = aContext; +} +ICCPDUHelperObject.prototype = { + context: null, + + /** + * Read GSM 8-bit unpacked octets, + * which are default 7-bit alphabets with bit 8 set to 0. + * + * @param numOctets + * Number of octets to be read. + */ + read8BitUnpackedToString: function(numOctets) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let ret = ""; + let escapeFound = false; + let i; + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + for(i = 0; i < numOctets; i++) { + let octet = GsmPDUHelper.readHexOctet(); + if (octet == 0xff) { + i++; + break; + } + + if (escapeFound) { + escapeFound = false; + if (octet == PDU_NL_EXTENDED_ESCAPE) { + // According to 3GPP TS 23.038, section 6.2.1.1, NOTE 1, "On + // receipt of this code, a receiving entity shall display a space + // until another extensiion table is defined." + ret += " "; + } else if (octet == PDU_NL_RESERVED_CONTROL) { + // According to 3GPP TS 23.038 B.2, "This code represents a control + // character and therefore must not be used for language specific + // characters." + ret += " "; + } else { + ret += langShiftTable[octet]; + } + } else if (octet == PDU_NL_EXTENDED_ESCAPE) { + escapeFound = true; + } else { + ret += langTable[octet]; + } + } + + let Buf = this.context.Buf; + Buf.seekIncoming((numOctets - i) * Buf.PDU_HEX_OCTET_SIZE); + return ret; + }, + + /** + * Write GSM 8-bit unpacked octets. + * + * @param numOctets Number of total octets to be writen, including trailing + * 0xff. + * @param str String to be written. Could be null. + * + * @return The string has been written into Buf. "" if str is null. + */ + writeStringTo8BitUnpacked: function(numOctets, str) { + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + let GsmPDUHelper = this.context.GsmPDUHelper; + + // If the character is GSM extended alphabet, two octets will be written. + // So we need to keep track of number of octets to be written. + let i, j; + let len = str ? str.length : 0; + for (i = 0, j = 0; i < len && j < numOctets; i++) { + let c = str.charAt(i); + let octet = langTable.indexOf(c); + + if (octet == -1) { + // Make sure we still have enough space to write two octets. + if (j + 2 > numOctets) { + break; + } + + octet = langShiftTable.indexOf(c); + if (octet == -1) { + // Fallback to ASCII space. + octet = langTable.indexOf(' '); + } else { + GsmPDUHelper.writeHexOctet(PDU_NL_EXTENDED_ESCAPE); + j++; + } + } + GsmPDUHelper.writeHexOctet(octet); + j++; + } + + // trailing 0xff + while (j++ < numOctets) { + GsmPDUHelper.writeHexOctet(0xff); + } + + return (str) ? str.substring(0, i) : ""; + }, + + /** + * Write UCS2 String on UICC. + * The default choose 0x81 or 0x82 encode, otherwise use 0x80 encode. + * + * @see TS 102.221, Annex A. + * @param numOctets + * Total number of octets to be written. This includes the length of + * alphaId and the length of trailing unused octets(0xff). + * @param str + * String to be written. + * + * @return The string has been written into Buf. + */ + writeICCUCS2String: function(numOctets, str) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let scheme = 0x80; + let basePointer; + + if (str.length > 2) { + let min = 0xFFFF; + let max = 0; + for (let i = 0; i < str.length; i++) { + let code = str.charCodeAt(i); + // filter out GSM Default Alphabet character + if (code & 0xFF80) { + if (min > code) { + min = code; + } + if (max < code) { + max = code; + } + } + } + + // 0x81 and 0x82 only support 'half-page', i.e., 128 characters. + if ((max - min) >= 0 && (max - min) < 128) { + // 0x81 base pointer is 0hhh hhhh h000 0000, and bit 16 is set to zero, + // therefore it can't compute 0x8000~0xFFFF. + // Since 0x81 only support 128 characters, + // either XX00~XX7f(bit 8 are 0) or XX80~XXff(bit 8 are 1) + if (((min & 0x7f80) == (max & 0x7f80)) && + ((max & 0x8000) == 0)) { + scheme = 0x81; + basePointer = min & 0x7f80; + } else { + scheme = 0x82; + basePointer = min; + } + } + } + + switch (scheme) { + /** + * +------+---------+---------+---------+---------+------+------+ + * | 0x80 | Ch1_msb | Ch1_lsb | Ch2_msb | Ch2_lsb | 0xff | 0xff | + * +------+---------+---------+---------+---------+------+------+ + */ + case 0x80: { + // 0x80 support UCS2 0000~ffff + GsmPDUHelper.writeHexOctet(0x80); + numOctets--; + // Now the str is UCS2 string, each character will take 2 octets. + if (str.length * 2 > numOctets) { + str = str.substring(0, Math.floor(numOctets / 2)); + } + GsmPDUHelper.writeUCS2String(str); + + // trailing 0xff + for (let i = str.length * 2; i < numOctets; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + return str; + } + /** + * +------+-----+--------------+-----+-----+-----+--------+------+ + * | 0x81 | len | base_pointer | Ch1 | Ch2 | ... | Ch_len | 0xff | + * +------+-----+--------------+-----+-----+-----+--------+------+ + * + * len: The length of characters. + * base_pointer: 0hhh hhhh h000 0000 + * Ch_n: bit 8 = 0 + * GSM default alphabets + * bit 8 = 1 + * UCS2 character whose char code is (Ch_n - base_pointer) | 0x80 + * + */ + case 0x81: { + GsmPDUHelper.writeHexOctet(0x81); + + if (str.length > (numOctets - 3)) { + str = str.substring(0, numOctets - 3); + } + + GsmPDUHelper.writeHexOctet(str.length); + GsmPDUHelper.writeHexOctet((basePointer >> 7) & 0xff); + numOctets -= 3; + break; + } + /* +------+-----+------------------+------------------+-----+-----+-----+--------+ + * | 0x82 | len | base_pointer_msb | base_pointer_lsb | Ch1 | Ch2 | ... | Ch_len | + * +------+-----+------------------+------------------+-----+-----+-----+--------+ + * + * len: The length of characters. + * base_pointer_msb, base_pointer_lsn: base_pointer + * Ch_n: bit 8 = 0 + * GSM default alphabets + * bit 8 = 1 + * UCS2 character whose char code is (Ch_n - base_pointer) | 0x80 + */ + case 0x82: { + GsmPDUHelper.writeHexOctet(0x82); + + if (str.length > (numOctets - 4)) { + str = str.substring(0, numOctets - 4); + } + + GsmPDUHelper.writeHexOctet(str.length); + GsmPDUHelper.writeHexOctet((basePointer >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(basePointer & 0xff); + numOctets -= 4; + break; + } + } + + if (scheme == 0x81 || scheme == 0x82) { + for (let i = 0; i < str.length; i++) { + let code = str.charCodeAt(i); + + // bit 8 = 0, + // GSM default alphabets + if (code >> 8 == 0) { + GsmPDUHelper.writeHexOctet(code & 0x7F); + } else { + // bit 8 = 1, + // UCS2 character whose char code is (code - basePointer) | 0x80 + GsmPDUHelper.writeHexOctet((code - basePointer) | 0x80); + } + } + + // trailing 0xff + for (let i = 0; i < numOctets - str.length; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + } + return str; + }, + + /** + * Read UCS2 String on UICC. + * + * @see TS 101.221, Annex A. + * @param scheme + * Coding scheme for UCS2 on UICC. One of 0x80, 0x81 or 0x82. + * @param numOctets + * Number of octets to be read as UCS2 string. + */ + readICCUCS2String: function(scheme, numOctets) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let str = ""; + switch (scheme) { + /** + * +------+---------+---------+---------+---------+------+------+ + * | 0x80 | Ch1_msb | Ch1_lsb | Ch2_msb | Ch2_lsb | 0xff | 0xff | + * +------+---------+---------+---------+---------+------+------+ + */ + case 0x80: + let isOdd = numOctets % 2; + let i; + for (i = 0; i < numOctets - isOdd; i += 2) { + let code = (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + if (code == 0xffff) { + i += 2; + break; + } + str += String.fromCharCode(code); + } + + // Skip trailing 0xff + Buf.seekIncoming((numOctets - i) * Buf.PDU_HEX_OCTET_SIZE); + break; + case 0x81: // Fall through + case 0x82: + /** + * +------+-----+--------+-----+-----+-----+--------+------+ + * | 0x81 | len | offset | Ch1 | Ch2 | ... | Ch_len | 0xff | + * +------+-----+--------+-----+-----+-----+--------+------+ + * + * len : The length of characters. + * offset : 0hhh hhhh h000 0000 + * Ch_n: bit 8 = 0 + * GSM default alphabets + * bit 8 = 1 + * UCS2 character whose char code is (Ch_n & 0x7f) + offset + * + * +------+-----+------------+------------+-----+-----+-----+--------+ + * | 0x82 | len | offset_msb | offset_lsb | Ch1 | Ch2 | ... | Ch_len | + * +------+-----+------------+------------+-----+-----+-----+--------+ + * + * len : The length of characters. + * offset_msb, offset_lsn: offset + * Ch_n: bit 8 = 0 + * GSM default alphabets + * bit 8 = 1 + * UCS2 character whose char code is (Ch_n & 0x7f) + offset + */ + let len = GsmPDUHelper.readHexOctet(); + let offset, headerLen; + if (scheme == 0x81) { + offset = GsmPDUHelper.readHexOctet() << 7; + headerLen = 2; + } else { + offset = (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + headerLen = 3; + } + + for (let i = 0; i < len; i++) { + let ch = GsmPDUHelper.readHexOctet(); + if (ch & 0x80) { + // UCS2 + str += String.fromCharCode((ch & 0x7f) + offset); + } else { + // GSM 8bit + let count = 0, gotUCS2 = 0; + while ((i + count + 1 < len)) { + count++; + if (GsmPDUHelper.readHexOctet() & 0x80) { + gotUCS2 = 1; + break; + } + } + // Unread. + // +1 for the GSM alphabet indexed at i, + Buf.seekIncoming(-1 * (count + 1) * Buf.PDU_HEX_OCTET_SIZE); + str += this.read8BitUnpackedToString(count + 1 - gotUCS2); + i += count - gotUCS2; + } + } + + // Skipping trailing 0xff + Buf.seekIncoming((numOctets - len - headerLen) * Buf.PDU_HEX_OCTET_SIZE); + break; + } + return str; + }, + + /** + * Read Alpha Id and Dialling number from TS TS 151.011 clause 10.5.1 + * + * @param recordSize The size of linear fixed record. + */ + readAlphaIdDiallingNumber: function(recordSize) { + let Buf = this.context.Buf; + let length = Buf.readInt32(); + + let alphaLen = recordSize - ADN_FOOTER_SIZE_BYTES; + let alphaId = this.readAlphaIdentifier(alphaLen); + + let number = this.readNumberWithLength(); + + // Skip unused octet, CCP + Buf.seekIncoming(Buf.PDU_HEX_OCTET_SIZE); + + let extRecordNumber = this.context.GsmPDUHelper.readHexOctet(); + Buf.readStringDelimiter(length); + + let contact = null; + if (alphaId || number) { + contact = {alphaId: alphaId, + number: number, + extRecordNumber: extRecordNumber}; + } + + return contact; + }, + + /** + * Write Alpha Identifier and Dialling number from TS 151.011 clause 10.5.1 + * + * @param recordSize The size of linear fixed record. + * @param alphaId Alpha Identifier to be written. + * @param number Dialling Number to be written. + * @param extRecordNumber The record identifier of the EXT. + * + * @return An object contains the alphaId and number + * that have been written into Buf. + */ + writeAlphaIdDiallingNumber: function(recordSize, alphaId, number, extRecordNumber) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + // Write String length + let strLen = recordSize * 2; + Buf.writeInt32(strLen); + + let alphaLen = recordSize - ADN_FOOTER_SIZE_BYTES; + let writtenAlphaId = this.writeAlphaIdentifier(alphaLen, alphaId); + let writtenNumber = this.writeNumberWithLength(number); + + // Write unused CCP octet 0xff. + GsmPDUHelper.writeHexOctet(0xff); + GsmPDUHelper.writeHexOctet((extRecordNumber != null) ? extRecordNumber : 0xff); + + Buf.writeStringDelimiter(strLen); + + return {alphaId: writtenAlphaId, + number: writtenNumber}; + }, + + /** + * Read Alpha Identifier. + * + * @see TS 131.102 + * + * @param numOctets + * Number of octets to be read. + * + * It uses either + * 1. SMS default 7-bit alphabet with bit 8 set to 0. + * 2. UCS2 string. + * + * Unused bytes should be set to 0xff. + */ + readAlphaIdentifier: function(numOctets) { + if (numOctets === 0) { + return ""; + } + + let temp; + // Read the 1st octet to determine the encoding. + if ((temp = this.context.GsmPDUHelper.readHexOctet()) == 0x80 || + temp == 0x81 || + temp == 0x82) { + numOctets--; + return this.readICCUCS2String(temp, numOctets); + } else { + let Buf = this.context.Buf; + Buf.seekIncoming(-1 * Buf.PDU_HEX_OCTET_SIZE); + return this.read8BitUnpackedToString(numOctets); + } + }, + + /** + * Write Alpha Identifier. + * + * @param numOctets + * Total number of octets to be written. This includes the length of + * alphaId and the length of trailing unused octets(0xff). + * @param alphaId + * Alpha Identifier to be written. + * + * @return The Alpha Identifier has been written into Buf. + * + * Unused octets will be written as 0xff. + */ + writeAlphaIdentifier: function(numOctets, alphaId) { + if (numOctets === 0) { + return ""; + } + + // If alphaId is empty or it's of GSM 8 bit. + if (!alphaId || this.context.ICCUtilsHelper.isGsm8BitAlphabet(alphaId)) { + return this.writeStringTo8BitUnpacked(numOctets, alphaId); + } else { + return this.writeICCUCS2String(numOctets, alphaId); + } + }, + + /** + * Read Dialling number. + * + * @see TS 131.102 + * + * @param len + * The Length of BCD number. + * + * From TS 131.102, in EF_ADN, EF_FDN, the field 'Length of BCD number' + * means the total bytes should be allocated to store the TON/NPI and + * the dialing number. + * For example, if the dialing number is 1234567890, + * and the TON/NPI is 0x81, + * The field 'Length of BCD number' should be 06, which is + * 1 byte to store the TON/NPI, 0x81 + * 5 bytes to store the BCD number 2143658709. + * + * Here the definition of the length is different from SMS spec, + * TS 23.040 9.1.2.5, which the length means + * "number of useful semi-octets within the Address-Value field". + */ + readDiallingNumber: function(len) { + if (DEBUG) this.context.debug("PDU: Going to read Dialling number: " + len); + if (len === 0) { + return ""; + } + + let GsmPDUHelper = this.context.GsmPDUHelper; + + // TOA = TON + NPI + let toa = GsmPDUHelper.readHexOctet(); + + let number = GsmPDUHelper.readSwappedNibbleExtendedBcdString(len - 1); + if (number.length <= 0) { + if (DEBUG) this.context.debug("No number provided"); + return ""; + } + if ((toa >> 4) == (PDU_TOA_INTERNATIONAL >> 4)) { + number = '+' + number; + } + return number; + }, + + /** + * Write Dialling Number. + * + * @param number The Dialling number + */ + writeDiallingNumber: function(number) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let toa = PDU_TOA_ISDN; // 81 + if (number[0] == '+') { + toa = PDU_TOA_INTERNATIONAL | PDU_TOA_ISDN; // 91 + number = number.substring(1); + } + GsmPDUHelper.writeHexOctet(toa); + GsmPDUHelper.writeSwappedNibbleBCD(number); + }, + + readNumberWithLength: function() { + let Buf = this.context.Buf; + let number = ""; + let numLen = this.context.GsmPDUHelper.readHexOctet(); + if (numLen != 0xff) { + if (numLen > ADN_MAX_BCD_NUMBER_BYTES) { + if (DEBUG) { + this.context.debug( + "Error: invalid length of BCD number/SSC contents - " + numLen); + } + Buf.seekIncoming(ADN_MAX_BCD_NUMBER_BYTES * Buf.PDU_HEX_OCTET_SIZE); + return number; + } + + number = this.readDiallingNumber(numLen); + Buf.seekIncoming((ADN_MAX_BCD_NUMBER_BYTES - numLen) * Buf.PDU_HEX_OCTET_SIZE); + } else { + Buf.seekIncoming(ADN_MAX_BCD_NUMBER_BYTES * Buf.PDU_HEX_OCTET_SIZE); + } + + return number; + }, + + /** + * Write Number with Length + * + * @param number The value to be written. + * + * @return The number has been written into Buf. + */ + writeNumberWithLength: function(number) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + if (number) { + let numStart = number[0] == "+" ? 1 : 0; + let writtenNumber = number.substring(0, numStart) + + number.substring(numStart) + .replace(/[^0-9*#,]/g, ""); + let numDigits = writtenNumber.length - numStart; + + if (numDigits > ADN_MAX_NUMBER_DIGITS) { + writtenNumber = writtenNumber.substring(0, ADN_MAX_NUMBER_DIGITS + numStart); + numDigits = writtenNumber.length - numStart; + } + + // +1 for TON/NPI + let numLen = Math.ceil(numDigits / 2) + 1; + GsmPDUHelper.writeHexOctet(numLen); + this.writeDiallingNumber(writtenNumber.replace(/\*/g, "a") + .replace(/\#/g, "b") + .replace(/\,/g, "c")); + // Write trailing 0xff of Dialling Number. + for (let i = 0; i < ADN_MAX_BCD_NUMBER_BYTES - numLen; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + return writtenNumber; + } else { + // +1 for numLen + for (let i = 0; i < ADN_MAX_BCD_NUMBER_BYTES + 1; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + return ""; + } + } +}; + +function StkCommandParamsFactoryObject(aContext) { + this.context = aContext; +} +StkCommandParamsFactoryObject.prototype = { + context: null, + + createParam: function(cmdDetails, ctlvs, onComplete) { + let method = this[cmdDetails.typeOfCommand]; + if (typeof method != "function") { + if (DEBUG) { + this.context.debug("Unknown proactive command " + + cmdDetails.typeOfCommand.toString(16)); + } + return; + } + method.call(this, cmdDetails, ctlvs, onComplete); + }, + + loadIcons: function(iconIdCtlvs, callback) { + if (!iconIdCtlvs || + !this.context.ICCUtilsHelper.isICCServiceAvailable("IMG")) { + callback(null); + return; + } + + let onerror = (function() { + callback(null); + }).bind(this); + + let onsuccess = (function(aIcons) { + callback(aIcons); + }).bind(this); + + this.context.IconLoader.loadIcons(iconIdCtlvs.map(aCtlv => aCtlv.value.identifier), + onsuccess, + onerror); + }, + + appendIconIfNecessary: function(iconIdCtlvs, result, onComplete) { + this.loadIcons(iconIdCtlvs, (aIcons) => { + if (aIcons) { + result.icons = aIcons[0]; + result.iconSelfExplanatory = + iconIdCtlvs[0].value.qualifier == 0 ? true : false; + } + + onComplete(result); + }); + }, + + /** + * Construct a param for Refresh. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processRefresh: function(cmdDetails, ctlvs, onComplete) { + let refreshType = cmdDetails.commandQualifier; + switch (refreshType) { + case STK_REFRESH_FILE_CHANGE: + case STK_REFRESH_NAA_INIT_AND_FILE_CHANGE: + let ctlv = this.context.StkProactiveCmdHelper.searchForTag( + COMPREHENSIONTLV_TAG_FILE_LIST, ctlvs); + if (ctlv) { + let list = ctlv.value.fileList; + if (DEBUG) { + this.context.debug("Refresh, list = " + list); + } + this.context.ICCRecordHelper.fetchICCRecords(); + } + break; + } + + onComplete(null); + }, + + /** + * Construct a param for Poll Interval. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processPollInterval: function(cmdDetails, ctlvs, onComplete) { + // Duration is mandatory. + let ctlv = this.context.StkProactiveCmdHelper.searchForTag( + COMPREHENSIONTLV_TAG_DURATION, ctlvs); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Poll Interval: Required value missing : Duration"); + } + + onComplete(ctlv.value); + }, + + /** + * Construct a param for Poll Off. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processPollOff: function(cmdDetails, ctlvs, onComplete) { + onComplete(null); + }, + + /** + * Construct a param for Set Up Event list. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSetUpEventList: function(cmdDetails, ctlvs, onComplete) { + // Event list is mandatory. + let ctlv = this.context.StkProactiveCmdHelper.searchForTag( + COMPREHENSIONTLV_TAG_EVENT_LIST, ctlvs); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Event List: Required value missing : Event List"); + } + + onComplete(ctlv.value || { eventList: null }); + }, + + /** + * Construct a param for Setup Menu. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSetupMenu: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let menu = { + // Help information available. + isHelpAvailable: !!(cmdDetails.commandQualifier & 0x80) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ITEM, + COMPREHENSIONTLV_TAG_ITEM_ID, + COMPREHENSIONTLV_TAG_NEXT_ACTION_IND, + COMPREHENSIONTLV_TAG_ICON_ID, + COMPREHENSIONTLV_TAG_ICON_ID_LIST + ]); + + // Alpha identifier is optional. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + menu.title = ctlv.value.identifier; + } + + // Item data object for item 1 is mandatory. + let menuCtlvs = selectedCtlvs[COMPREHENSIONTLV_TAG_ITEM]; + if (!menuCtlvs) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Menu: Required value missing : items"); + } + menu.items = menuCtlvs.map(aCtlv => aCtlv.value); + + // Item identifier is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ITEM_ID); + if (ctlv) { + menu.defaultItem = ctlv.value.identifier - 1; + } + + // Items next action indicator is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_NEXT_ACTION_IND); + if (ctlv) { + menu.nextActionList = ctlv.value; + } + + // Icon identifier is optional. + let iconIdCtlvs = null; + let menuIconCtlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ICON_ID); + if (menuIconCtlv) { + iconIdCtlvs = [menuIconCtlv]; + } + + // Item icon identifier list is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ICON_ID_LIST); + if (ctlv) { + if (!iconIdCtlvs) { + iconIdCtlvs = []; + }; + let iconIdList = ctlv.value; + iconIdCtlvs = iconIdCtlvs.concat(iconIdList.identifiers.map((aId) => { + return { + value: { qualifier: iconIdList.qualifier, identifier: aId } + }; + })); + } + + this.loadIcons(iconIdCtlvs, (aIcons) => { + if (aIcons) { + if (menuIconCtlv) { + menu.iconSelfExplanatory = + (iconIdCtlvs.shift().value.qualifier == 0) ? true: false; + menu.icons = aIcons.shift(); + } + + for (let i = 0; i < aIcons.length; i++) { + menu.items[i].icons = aIcons[i]; + menu.items[i].iconSelfExplanatory = + (iconIdCtlvs[i].value.qualifier == 0) ? true: false; + } + } + + onComplete(menu); + }); + }, + + /** + * Construct a param for Select Item. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSelectItem: function(cmdDetails, ctlvs, onComplete) { + this.processSetupMenu(cmdDetails, ctlvs, (menu) => { + // The 1st bit and 2nd bit determines the presentation type. + menu.presentationType = cmdDetails.commandQualifier & 0x03; + onComplete(menu); + }); + }, + + /** + * Construct a param for Display Text. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processDisplayText: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let textMsg = { + isHighPriority: !!(cmdDetails.commandQualifier & 0x01), + userClear: !!(cmdDetails.commandQualifier & 0x80) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TEXT_STRING, + COMPREHENSIONTLV_TAG_IMMEDIATE_RESPONSE, + COMPREHENSIONTLV_TAG_DURATION, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Text string is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TEXT_STRING); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Display Text: Required value missing : Text String"); + } + textMsg.text = ctlv.value.textString; + + // Immediate response is optional. + textMsg.responseNeeded = + !!(selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_IMMEDIATE_RESPONSE)); + + // Duration is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DURATION); + if (ctlv) { + textMsg.duration = ctlv.value; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + textMsg, + onComplete); + }, + + /** + * Construct a param for Setup Idle Mode Text. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSetUpIdleModeText: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let textMsg = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TEXT_STRING, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Text string is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TEXT_STRING); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Set Up Idle Text: Required value missing : Text String"); + } + textMsg.text = ctlv.value.textString; + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + textMsg, + onComplete); + }, + + /** + * Construct a param for Get Inkey. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processGetInkey: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let input = { + minLength: 1, + maxLength: 1, + isAlphabet: !!(cmdDetails.commandQualifier & 0x01), + isUCS2: !!(cmdDetails.commandQualifier & 0x02), + // Character sets defined in bit 1 and bit 2 are disable and + // the YES/NO reponse is required. + isYesNoRequested: !!(cmdDetails.commandQualifier & 0x04), + // Help information available. + isHelpAvailable: !!(cmdDetails.commandQualifier & 0x80) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TEXT_STRING, + COMPREHENSIONTLV_TAG_DURATION, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Text string is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TEXT_STRING); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Get InKey: Required value missing : Text String"); + } + input.text = ctlv.value.textString; + + // Duration is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DURATION); + if (ctlv) { + input.duration = ctlv.value; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + input, + onComplete); + }, + + /** + * Construct a param for Get Input. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processGetInput: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let input = { + isAlphabet: !!(cmdDetails.commandQualifier & 0x01), + isUCS2: !!(cmdDetails.commandQualifier & 0x02), + // User input shall not be revealed + hideInput: !!(cmdDetails.commandQualifier & 0x04), + // User input in SMS packed format + isPacked: !!(cmdDetails.commandQualifier & 0x08), + // Help information available. + isHelpAvailable: !!(cmdDetails.commandQualifier & 0x80) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TEXT_STRING, + COMPREHENSIONTLV_TAG_RESPONSE_LENGTH, + COMPREHENSIONTLV_TAG_DEFAULT_TEXT, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Text string is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TEXT_STRING); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Get Input: Required value missing : Text String"); + } + input.text = ctlv.value.textString; + + // Response length is mandatory. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_RESPONSE_LENGTH); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Get Input: Required value missing : Response Length"); + } + input.minLength = ctlv.value.minLength; + input.maxLength = ctlv.value.maxLength; + + // Default text is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DEFAULT_TEXT); + if (ctlv) { + input.defaultText = ctlv.value.textString; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + input, + onComplete); + }, + + /** + * Construct a param for SendSS/SMS/USSD/DTMF. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processEventNotify: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let textMsg = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Alpha identifier is optional. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + textMsg.text = ctlv.value.identifier; + } + + // According to section 6.4.10 of |ETSI TS 102 223|: + // + // - if the alpha identifier is provided by the UICC and is a null data + // object (i.e. length = '00' and no value part), this is an indication + // that the terminal should not give any information to the user on the + // fact that the terminal is sending a short message; + // + // - if the alpha identifier is not provided by the UICC, the terminal may + // give information to the user concerning what is happening. + // + // ICCPDUHelper reads alpha id as an empty string if the length is zero, + // hence we'll notify the caller when it's not an empty string. + if (textMsg.text !== "") { + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + textMsg, + onComplete); + } + }, + + /** + * Construct a param for Setup Call. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processSetupCall: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let call = {}; + let confirmMessage = {}; + let callMessage = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ADDRESS, + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ICON_ID, + COMPREHENSIONTLV_TAG_DURATION + ]); + + // Address is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ADDRESS); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Set Up Call: Required value missing : Address"); + } + call.address = ctlv.value.number; + + // Alpha identifier (user confirmation phase) is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + confirmMessage.text = ctlv.value.identifier; + call.confirmMessage = confirmMessage; + } + + // Alpha identifier (call set up phase) is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + callMessage.text = ctlv.value.identifier; + call.callMessage = callMessage; + } + + // Duration is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DURATION); + if (ctlv) { + call.duration = ctlv.value; + } + + // Icon identifier is optional. + let iconIdCtlvs = selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null; + this.loadIcons(iconIdCtlvs, (aIcons) => { + if (aIcons) { + confirmMessage.icons = aIcons[0]; + confirmMessage.iconSelfExplanatory = + (iconIdCtlvs[0].value.qualifier == 0) ? true: false; + call.confirmMessage = confirmMessage; + + if (aIcons.length > 1) { + callMessage.icons = aIcons[1]; + callMessage.iconSelfExplanatory = + (iconIdCtlvs[1].value.qualifier == 0) ? true: false; + call.callMessage = callMessage; + } + } + + onComplete(call); + }); + }, + + /** + * Construct a param for Launch Browser. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processLaunchBrowser: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let browser = { + mode: cmdDetails.commandQualifier & 0x03 + }; + let confirmMessage = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_URL, + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // URL is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_URL); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Launch Browser: Required value missing : URL"); + } + browser.url = ctlv.value.url; + + // Alpha identifier is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + confirmMessage.text = ctlv.value.identifier; + browser.confirmMessage = confirmMessage; + } + + // Icon identifier is optional. + let iconIdCtlvs = selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null; + this.loadIcons(iconIdCtlvs, (aIcons) => { + if (aIcons) { + confirmMessage.icons = aIcons[0]; + confirmMessage.iconSelfExplanatory = + (iconIdCtlvs[0].value.qualifier == 0) ? true: false; + browser.confirmMessage = confirmMessage; + } + + onComplete(browser); + }); + }, + + /** + * Construct a param for Play Tone. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processPlayTone: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let playTone = { + // The vibrate is only defined in TS 102.223. + isVibrate: !!(cmdDetails.commandQualifier & 0x01) + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_TONE, + COMPREHENSIONTLV_TAG_DURATION, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Alpha identifier is optional. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + playTone.text = ctlv.value.identifier; + } + + // Tone is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TONE); + if (ctlv) { + playTone.tone = ctlv.value.tone; + } + + // Duration is optional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_DURATION); + if (ctlv) { + playTone.duration = ctlv.value; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + playTone, + onComplete); + }, + + /** + * Construct a param for Provide Local Information. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processProvideLocalInfo: function(cmdDetails, ctlvs, onComplete) { + let provideLocalInfo = { + localInfoType: cmdDetails.commandQualifier + }; + + onComplete(provideLocalInfo); + }, + + /** + * Construct a param for Timer Management. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processTimerManagement: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let timer = { + timerAction: cmdDetails.commandQualifier + }; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER, + COMPREHENSIONTLV_TAG_TIMER_VALUE + ]); + + // Timer identifier is mandatory. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER); + if (!ctlv) { + this.context.RIL.sendStkTerminalResponse({ + command: cmdDetails, + resultCode: STK_RESULT_REQUIRED_VALUES_MISSING}); + throw new Error("Stk Timer Management: Required value missing : Timer Identifier"); + } + timer.timerId = ctlv.value.timerId; + + // Timer value is conditional. + ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_TIMER_VALUE); + if (ctlv) { + timer.timerValue = ctlv.value.timerValue; + } + + onComplete(timer); + }, + + /** + * Construct a param for BIP commands. + * + * @param cmdDetails + * The value object of CommandDetails TLV. + * @param ctlvs + * The all TLVs in this proactive command. + * @param onComplete + * Callback to be called when complete. + */ + processBipMessage: function(cmdDetails, ctlvs, onComplete) { + let StkProactiveCmdHelper = this.context.StkProactiveCmdHelper; + let bipMsg = {}; + + let selectedCtlvs = StkProactiveCmdHelper.searchForSelectedTags(ctlvs, [ + COMPREHENSIONTLV_TAG_ALPHA_ID, + COMPREHENSIONTLV_TAG_ICON_ID + ]); + + // Alpha identifier is optional. + let ctlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + if (ctlv) { + bipMsg.text = ctlv.value.identifier; + } + + // Icon identifier is optional. + this.appendIconIfNecessary(selectedCtlvs[COMPREHENSIONTLV_TAG_ICON_ID] || null, + bipMsg, + onComplete); + } +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_REFRESH] = function STK_CMD_REFRESH(cmdDetails, ctlvs, onComplete) { + return this.processRefresh(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_POLL_INTERVAL] = function STK_CMD_POLL_INTERVAL(cmdDetails, ctlvs, onComplete) { + return this.processPollInterval(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_POLL_OFF] = function STK_CMD_POLL_OFF(cmdDetails, ctlvs, onComplete) { + return this.processPollOff(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_PROVIDE_LOCAL_INFO] = function STK_CMD_PROVIDE_LOCAL_INFO(cmdDetails, ctlvs, onComplete) { + return this.processProvideLocalInfo(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SET_UP_EVENT_LIST] = function STK_CMD_SET_UP_EVENT_LIST(cmdDetails, ctlvs, onComplete) { + return this.processSetUpEventList(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SET_UP_MENU] = function STK_CMD_SET_UP_MENU(cmdDetails, ctlvs, onComplete) { + return this.processSetupMenu(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SELECT_ITEM] = function STK_CMD_SELECT_ITEM(cmdDetails, ctlvs, onComplete) { + return this.processSelectItem(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_DISPLAY_TEXT] = function STK_CMD_DISPLAY_TEXT(cmdDetails, ctlvs, onComplete) { + return this.processDisplayText(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SET_UP_IDLE_MODE_TEXT] = function STK_CMD_SET_UP_IDLE_MODE_TEXT(cmdDetails, ctlvs, onComplete) { + return this.processSetUpIdleModeText(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_GET_INKEY] = function STK_CMD_GET_INKEY(cmdDetails, ctlvs, onComplete) { + return this.processGetInkey(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_GET_INPUT] = function STK_CMD_GET_INPUT(cmdDetails, ctlvs, onComplete) { + return this.processGetInput(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_SS] = function STK_CMD_SEND_SS(cmdDetails, ctlvs, onComplete) { + return this.processEventNotify(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_USSD] = function STK_CMD_SEND_USSD(cmdDetails, ctlvs, onComplete) { + return this.processEventNotify(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_SMS] = function STK_CMD_SEND_SMS(cmdDetails, ctlvs, onComplete) { + return this.processEventNotify(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_DTMF] = function STK_CMD_SEND_DTMF(cmdDetails, ctlvs, onComplete) { + return this.processEventNotify(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SET_UP_CALL] = function STK_CMD_SET_UP_CALL(cmdDetails, ctlvs, onComplete) { + return this.processSetupCall(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_LAUNCH_BROWSER] = function STK_CMD_LAUNCH_BROWSER(cmdDetails, ctlvs, onComplete) { + return this.processLaunchBrowser(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_PLAY_TONE] = function STK_CMD_PLAY_TONE(cmdDetails, ctlvs, onComplete) { + return this.processPlayTone(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_TIMER_MANAGEMENT] = function STK_CMD_TIMER_MANAGEMENT(cmdDetails, ctlvs, onComplete) { + return this.processTimerManagement(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_OPEN_CHANNEL] = function STK_CMD_OPEN_CHANNEL(cmdDetails, ctlvs, onComplete) { + return this.processBipMessage(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_CLOSE_CHANNEL] = function STK_CMD_CLOSE_CHANNEL(cmdDetails, ctlvs, onComplete) { + return this.processBipMessage(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_RECEIVE_DATA] = function STK_CMD_RECEIVE_DATA(cmdDetails, ctlvs, onComplete) { + return this.processBipMessage(cmdDetails, ctlvs, onComplete); +}; +StkCommandParamsFactoryObject.prototype[STK_CMD_SEND_DATA] = function STK_CMD_SEND_DATA(cmdDetails, ctlvs, onComplete) { + return this.processBipMessage(cmdDetails, ctlvs, onComplete); +}; + +function StkProactiveCmdHelperObject(aContext) { + this.context = aContext; +} +StkProactiveCmdHelperObject.prototype = { + context: null, + + retrieve: function(tag, length) { + let method = this[tag]; + if (typeof method != "function") { + if (DEBUG) { + this.context.debug("Unknown comprehension tag " + tag.toString(16)); + } + let Buf = this.context.Buf; + Buf.seekIncoming(length * Buf.PDU_HEX_OCTET_SIZE); + return null; + } + return method.call(this, length); + }, + + /** + * Command Details. + * + * | Byte | Description | Length | + * | 1 | Command details Tag | 1 | + * | 2 | Length = 03 | 1 | + * | 3 | Command number | 1 | + * | 4 | Type of Command | 1 | + * | 5 | Command Qualifier | 1 | + */ + retrieveCommandDetails: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let cmdDetails = { + commandNumber: GsmPDUHelper.readHexOctet(), + typeOfCommand: GsmPDUHelper.readHexOctet(), + commandQualifier: GsmPDUHelper.readHexOctet() + }; + return cmdDetails; + }, + + /** + * Device Identities. + * + * | Byte | Description | Length | + * | 1 | Device Identity Tag | 1 | + * | 2 | Length = 02 | 1 | + * | 3 | Source device Identity | 1 | + * | 4 | Destination device Id | 1 | + */ + retrieveDeviceId: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let deviceId = { + sourceId: GsmPDUHelper.readHexOctet(), + destinationId: GsmPDUHelper.readHexOctet() + }; + return deviceId; + }, + + /** + * Alpha Identifier. + * + * | Byte | Description | Length | + * | 1 | Alpha Identifier Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 ~ | Alpha identfier | X | + * | (Y-1)+X+2 | | | + */ + retrieveAlphaId: function(length) { + let alphaId = { + identifier: this.context.ICCPDUHelper.readAlphaIdentifier(length) + }; + return alphaId; + }, + + /** + * Duration. + * + * | Byte | Description | Length | + * | 1 | Response Length Tag | 1 | + * | 2 | Lenth = 02 | 1 | + * | 3 | Time unit | 1 | + * | 4 | Time interval | 1 | + */ + retrieveDuration: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let duration = { + timeUnit: GsmPDUHelper.readHexOctet(), + timeInterval: GsmPDUHelper.readHexOctet(), + }; + return duration; + }, + + /** + * Address. + * + * | Byte | Description | Length | + * | 1 | Alpha Identifier Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 | TON and NPI | 1 | + * | (Y-1)+4 ~ | Dialling number | X | + * | (Y-1)+X+2 | | | + */ + retrieveAddress: function(length) { + let address = { + number : this.context.ICCPDUHelper.readDiallingNumber(length) + }; + return address; + }, + + /** + * Text String. + * + * | Byte | Description | Length | + * | 1 | Text String Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 | Data coding scheme | 1 | + * | (Y-1)+4~ | Text String | X | + * | (Y-1)+X+2 | | | + */ + retrieveTextString: function(length) { + if (!length) { + // null string. + return {textString: null}; + } + + let GsmPDUHelper = this.context.GsmPDUHelper; + let text = { + codingScheme: GsmPDUHelper.readHexOctet() + }; + + length--; // -1 for the codingScheme. + switch (text.codingScheme & 0x0c) { + case STK_TEXT_CODING_GSM_7BIT_PACKED: + text.textString = + GsmPDUHelper.readSeptetsToString(Math.floor(length * 8 / 7), 0, 0, 0); + break; + case STK_TEXT_CODING_GSM_8BIT: + text.textString = + this.context.ICCPDUHelper.read8BitUnpackedToString(length); + break; + case STK_TEXT_CODING_UCS2: + text.textString = GsmPDUHelper.readUCS2String(length); + break; + } + return text; + }, + + /** + * Tone. + * + * | Byte | Description | Length | + * | 1 | Tone Tag | 1 | + * | 2 | Lenth = 01 | 1 | + * | 3 | Tone | 1 | + */ + retrieveTone: function(length) { + let tone = { + tone: this.context.GsmPDUHelper.readHexOctet(), + }; + return tone; + }, + + /** + * Item. + * + * | Byte | Description | Length | + * | 1 | Item Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 | Identifier of item | 1 | + * | (Y-1)+4 ~ | Text string of item | X | + * | (Y-1)+X+2 | | | + */ + retrieveItem: function(length) { + // TS 102.223 ,clause 6.6.7 SET-UP MENU + // If the "Item data object for item 1" is a null data object + // (i.e. length = '00' and no value part), this is an indication to the ME + // to remove the existing menu from the menu system in the ME. + if (!length) { + return null; + } + let item = { + identifier: this.context.GsmPDUHelper.readHexOctet(), + text: this.context.ICCPDUHelper.readAlphaIdentifier(length - 1) + }; + return item; + }, + + /** + * Item Identifier. + * + * | Byte | Description | Length | + * | 1 | Item Identifier Tag | 1 | + * | 2 | Lenth = 01 | 1 | + * | 3 | Identifier of Item chosen | 1 | + */ + retrieveItemId: function(length) { + let itemId = { + identifier: this.context.GsmPDUHelper.readHexOctet() + }; + return itemId; + }, + + /** + * Response Length. + * + * | Byte | Description | Length | + * | 1 | Response Length Tag | 1 | + * | 2 | Lenth = 02 | 1 | + * | 3 | Minimum length of response | 1 | + * | 4 | Maximum length of response | 1 | + */ + retrieveResponseLength: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let rspLength = { + minLength : GsmPDUHelper.readHexOctet(), + maxLength : GsmPDUHelper.readHexOctet() + }; + return rspLength; + }, + + /** + * File List. + * + * | Byte | Description | Length | + * | 1 | File List Tag | 1 | + * | 2 ~ (Y-1)+2 | Length (X) | Y | + * | (Y-1)+3 | Number of files | 1 | + * | (Y-1)+4 ~ | Files | X | + * | (Y-1)+X+2 | | | + */ + retrieveFileList: function(length) { + let num = this.context.GsmPDUHelper.readHexOctet(); + let fileList = ""; + length--; // -1 for the num octet. + for (let i = 0; i < 2 * length; i++) { + // Didn't use readHexOctet here, + // otherwise 0x00 will be "0", not "00" + fileList += String.fromCharCode(this.context.Buf.readUint16()); + } + return { + fileList: fileList + }; + }, + + /** + * Default Text. + * + * Same as Text String. + */ + retrieveDefaultText: function(length) { + return this.retrieveTextString(length); + }, + + /** + * Event List. + */ + retrieveEventList: function(length) { + if (!length) { + // null means an indication to ME to remove the existing list of events + // in ME. + return null; + } + + let GsmPDUHelper = this.context.GsmPDUHelper; + let eventList = []; + for (let i = 0; i < length; i++) { + eventList.push(GsmPDUHelper.readHexOctet()); + } + return { + eventList: eventList + }; + }, + + /** + * Icon Id. + * + * | Byte | Description | Length | + * | 1 | Icon Identifier Tag | 1 | + * | 2 | Length = 02 | 1 | + * | 3 | Icon qualifier | 1 | + * | 4 | Icon identifier | 1 | + */ + retrieveIconId: function(length) { + if (!length) { + return null; + } + + let iconId = { + qualifier: this.context.GsmPDUHelper.readHexOctet(), + identifier: this.context.GsmPDUHelper.readHexOctet() + }; + return iconId; + }, + + /** + * Icon Id List. + * + * | Byte | Description | Length | + * | 1 | Icon Identifier Tag | 1 | + * | 2 | Length = X | 1 | + * | 3 | Icon qualifier | 1 | + * | 4~ | Icon identifier | X-1 | + * | 4+X-2 | | | + */ + retrieveIconIdList: function(length) { + if (!length) { + return null; + } + + let iconIdList = { + qualifier: this.context.GsmPDUHelper.readHexOctet(), + identifiers: [] + }; + for (let i = 0; i < length - 1; i++) { + iconIdList.identifiers.push(this.context.GsmPDUHelper.readHexOctet()); + } + return iconIdList; + }, + + /** + * Timer Identifier. + * + * | Byte | Description | Length | + * | 1 | Timer Identifier Tag | 1 | + * | 2 | Length = 01 | 1 | + * | 3 | Timer Identifier | 1 | + */ + retrieveTimerId: function(length) { + let id = { + timerId: this.context.GsmPDUHelper.readHexOctet() + }; + return id; + }, + + /** + * Timer Value. + * + * | Byte | Description | Length | + * | 1 | Timer Value Tag | 1 | + * | 2 | Length = 03 | 1 | + * | 3 | Hour | 1 | + * | 4 | Minute | 1 | + * | 5 | Second | 1 | + */ + retrieveTimerValue: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let value = { + timerValue: (GsmPDUHelper.readSwappedNibbleBcdNum(1) * 60 * 60) + + (GsmPDUHelper.readSwappedNibbleBcdNum(1) * 60) + + (GsmPDUHelper.readSwappedNibbleBcdNum(1)) + }; + return value; + }, + + /** + * Immediate Response. + * + * | Byte | Description | Length | + * | 1 | Immediate Response Tag | 1 | + * | 2 | Length = 00 | 1 | + */ + retrieveImmediaResponse: function(length) { + return {}; + }, + + /** + * URL + * + * | Byte | Description | Length | + * | 1 | URL Tag | 1 | + * | 2 ~ (Y+1) | Length(X) | Y | + * | (Y+2) ~ | URL | X | + * | (Y+1+X) | | | + */ + retrieveUrl: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let s = ""; + for (let i = 0; i < length; i++) { + s += String.fromCharCode(GsmPDUHelper.readHexOctet()); + } + return {url: s}; + }, + + /** + * Next Action Indicator List. + * + * | Byte | Description | Length | + * | 1 | Next Action tag | 1 | + * | 1 | Length(X) | 1 | + * | 3~ | Next Action List | X | + * | 3+X-1 | | | + */ + retrieveNextActionList: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let nextActionList = []; + for (let i = 0; i < length; i++) { + nextActionList.push(GsmPDUHelper.readHexOctet()); + } + return nextActionList; + }, + + searchForTag: function(tag, ctlvs) { + for (let ctlv of ctlvs) { + if ((ctlv.tag & ~COMPREHENSIONTLV_FLAG_CR) == tag) { + return ctlv; + } + } + return null; + }, + + searchForSelectedTags: function(ctlvs, tags) { + let ret = { + // Handy utility to de-queue the 1st ctlv of the specified tag. + retrieve: function(aTag) { + return (this[aTag]) ? this[aTag].shift() : null; + } + }; + + ctlvs.forEach((aCtlv) => { + tags.forEach((aTag) => { + if ((aCtlv.tag & ~COMPREHENSIONTLV_FLAG_CR) == aTag) { + if (!ret[aTag]) { + ret[aTag] = []; + } + ret[aTag].push(aCtlv); + } + }); + }); + + return ret; + }, +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_COMMAND_DETAILS] = function COMPREHENSIONTLV_TAG_COMMAND_DETAILS(length) { + return this.retrieveCommandDetails(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_DEVICE_ID] = function COMPREHENSIONTLV_TAG_DEVICE_ID(length) { + return this.retrieveDeviceId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ALPHA_ID] = function COMPREHENSIONTLV_TAG_ALPHA_ID(length) { + return this.retrieveAlphaId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_DURATION] = function COMPREHENSIONTLV_TAG_DURATION(length) { + return this.retrieveDuration(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ADDRESS] = function COMPREHENSIONTLV_TAG_ADDRESS(length) { + return this.retrieveAddress(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_TEXT_STRING] = function COMPREHENSIONTLV_TAG_TEXT_STRING(length) { + return this.retrieveTextString(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_TONE] = function COMPREHENSIONTLV_TAG_TONE(length) { + return this.retrieveTone(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ITEM] = function COMPREHENSIONTLV_TAG_ITEM(length) { + return this.retrieveItem(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ITEM_ID] = function COMPREHENSIONTLV_TAG_ITEM_ID(length) { + return this.retrieveItemId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_RESPONSE_LENGTH] = function COMPREHENSIONTLV_TAG_RESPONSE_LENGTH(length) { + return this.retrieveResponseLength(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_FILE_LIST] = function COMPREHENSIONTLV_TAG_FILE_LIST(length) { + return this.retrieveFileList(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_DEFAULT_TEXT] = function COMPREHENSIONTLV_TAG_DEFAULT_TEXT(length) { + return this.retrieveDefaultText(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_EVENT_LIST] = function COMPREHENSIONTLV_TAG_EVENT_LIST(length) { + return this.retrieveEventList(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ICON_ID] = function COMPREHENSIONTLV_TAG_ICON_ID(length) { + return this.retrieveIconId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_ICON_ID_LIST] = function COMPREHENSIONTLV_TAG_ICON_ID_LIST(length) { + return this.retrieveIconIdList(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER] = function COMPREHENSIONTLV_TAG_TIMER_IDENTIFIER(length) { + return this.retrieveTimerId(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_TIMER_VALUE] = function COMPREHENSIONTLV_TAG_TIMER_VALUE(length) { + return this.retrieveTimerValue(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_IMMEDIATE_RESPONSE] = function COMPREHENSIONTLV_TAG_IMMEDIATE_RESPONSE(length) { + return this.retrieveImmediaResponse(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_URL] = function COMPREHENSIONTLV_TAG_URL(length) { + return this.retrieveUrl(length); +}; +StkProactiveCmdHelperObject.prototype[COMPREHENSIONTLV_TAG_NEXT_ACTION_IND] = function COMPREHENSIONTLV_TAG_NEXT_ACTION_IND(length) { + return this.retrieveNextActionList(length); +}; + +function ComprehensionTlvHelperObject(aContext) { + this.context = aContext; +} +ComprehensionTlvHelperObject.prototype = { + context: null, + + /** + * Decode raw data to a Comprehension-TLV. + */ + decode: function() { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let hlen = 0; // For header(tag field + length field) length. + let temp = GsmPDUHelper.readHexOctet(); + hlen++; + + // TS 101.220, clause 7.1.1 + let tag, cr; + switch (temp) { + // TS 101.220, clause 7.1.1 + case 0x0: // Not used. + case 0xff: // Not used. + case 0x80: // Reserved for future use. + throw new Error("Invalid octet when parsing Comprehension TLV :" + temp); + case 0x7f: // Tag is three byte format. + // TS 101.220 clause 7.1.1.2. + // | Byte 1 | Byte 2 | Byte 3 | + // | | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | | + // | 0x7f |CR | Tag Value | + tag = (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + hlen += 2; + cr = (tag & 0x8000) !== 0; + tag &= ~0x8000; + break; + default: // Tag is single byte format. + tag = temp; + // TS 101.220 clause 7.1.1.1. + // | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | + // |CR | Tag Value | + cr = (tag & 0x80) !== 0; + tag &= ~0x80; + } + + // TS 101.220 clause 7.1.2, Length Encoding. + // Length | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + // 0 - 127 | 00 - 7f | N/A | N/A | N/A | + // 128-255 | 81 | 80 - ff| N/A | N/A | + // 256-65535| 82 | 0100 - ffff | N/A | + // 65536- | 83 | 010000 - ffffff | + // 16777215 + // + // Length errors: TS 11.14, clause 6.10.6 + + let length; // Data length. + temp = GsmPDUHelper.readHexOctet(); + hlen++; + if (temp < 0x80) { + length = temp; + } else if (temp == 0x81) { + length = GsmPDUHelper.readHexOctet(); + hlen++; + if (length < 0x80) { + throw new Error("Invalid length in Comprehension TLV :" + length); + } + } else if (temp == 0x82) { + length = (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + hlen += 2; + if (lenth < 0x0100) { + throw new Error("Invalid length in 3-byte Comprehension TLV :" + length); + } + } else if (temp == 0x83) { + length = (GsmPDUHelper.readHexOctet() << 16) | + (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(); + hlen += 3; + if (length < 0x010000) { + throw new Error("Invalid length in 4-byte Comprehension TLV :" + length); + } + } else { + throw new Error("Invalid octet in Comprehension TLV :" + temp); + } + + let ctlv = { + tag: tag, + length: length, + value: this.context.StkProactiveCmdHelper.retrieve(tag, length), + cr: cr, + hlen: hlen + }; + return ctlv; + }, + + decodeChunks: function(length) { + let chunks = []; + let index = 0; + while (index < length) { + let tlv = this.decode(); + chunks.push(tlv); + index += tlv.length; + index += tlv.hlen; + } + return chunks; + }, + + /** + * Write Location Info Comprehension TLV. + * + * @param loc location Information. + */ + writeLocationInfoTlv: function(loc) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_LOCATION_INFO | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(loc.gsmCellId > 0xffff ? 9 : 7); + // From TS 11.14, clause 12.19 + // "The mobile country code (MCC), the mobile network code (MNC), + // the location area code (LAC) and the cell ID are + // coded as in TS 04.08." + // And from TS 04.08 and TS 24.008, + // the format is as follows: + // + // MCC = MCC_digit_1 + MCC_digit_2 + MCC_digit_3 + // + // 8 7 6 5 4 3 2 1 + // +-------------+-------------+ + // | MCC digit 2 | MCC digit 1 | octet 2 + // | MNC digit 3 | MCC digit 3 | octet 3 + // | MNC digit 2 | MNC digit 1 | octet 4 + // +-------------+-------------+ + // + // Also in TS 24.008 + // "However a network operator may decide to + // use only two digits in the MNC in the LAI over the + // radio interface. In this case, bits 5 to 8 of octet 3 + // shall be coded as '1111'". + + // MCC & MNC, 3 octets + let mcc = loc.mcc, mnc; + if (loc.mnc.length == 2) { + mnc = "F" + loc.mnc; + } else { + mnc = loc.mnc[2] + loc.mnc[0] + loc.mnc[1]; + } + GsmPDUHelper.writeSwappedNibbleBCD(mcc + mnc); + + // LAC, 2 octets + GsmPDUHelper.writeHexOctet((loc.gsmLocationAreaCode >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(loc.gsmLocationAreaCode & 0xff); + + // Cell Id + if (loc.gsmCellId > 0xffff) { + // UMTS/WCDMA, gsmCellId is 28 bits. + GsmPDUHelper.writeHexOctet((loc.gsmCellId >> 24) & 0xff); + GsmPDUHelper.writeHexOctet((loc.gsmCellId >> 16) & 0xff); + GsmPDUHelper.writeHexOctet((loc.gsmCellId >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(loc.gsmCellId & 0xff); + } else { + // GSM, gsmCellId is 16 bits. + GsmPDUHelper.writeHexOctet((loc.gsmCellId >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(loc.gsmCellId & 0xff); + } + }, + + /** + * Given a geckoError string, this function translates it into cause value + * and write the value into buffer. + * + * @param geckoError Error string that is passed to gecko. + */ + writeCauseTlv: function(geckoError) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let cause = -1; + for (let errorNo in RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR) { + if (geckoError == RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[errorNo]) { + cause = errorNo; + break; + } + } + + // Causes specified in 10.5.4.11 of TS 04.08 are less than 128. + // we can ignore causes > 127 since Cause TLV is optional in + // STK_EVENT_TYPE_CALL_DISCONNECTED. + if (cause > 127) { + return; + } + + cause = (cause == -1) ? ERROR_SUCCESS : cause; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_CAUSE | + COMPREHENSIONTLV_FLAG_CR); + GsmPDUHelper.writeHexOctet(2); // For single cause value. + + // TS 04.08, clause 10.5.4.11: + // Code Standard : Standard defined for GSM PLMNS + // Location: User + GsmPDUHelper.writeHexOctet(0x60); + + // TS 04.08, clause 10.5.4.11: ext bit = 1 + 7 bits for cause. + // +-----------------+----------------------------------+ + // | Ext = 1 (1 bit) | Cause (7 bits) | + // +-----------------+----------------------------------+ + GsmPDUHelper.writeHexOctet(0x80 | cause); + }, + + writeDateTimeZoneTlv: function(date) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_DATE_TIME_ZONE); + GsmPDUHelper.writeHexOctet(7); + GsmPDUHelper.writeTimestamp(date); + }, + + writeLanguageTlv: function(language) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_LANGUAGE); + GsmPDUHelper.writeHexOctet(2); + + // ISO 639-1, Alpha-2 code + // TS 123.038, clause 6.2.1, GSM 7 bit Default Alphabet + GsmPDUHelper.writeHexOctet( + PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT].indexOf(language[0])); + GsmPDUHelper.writeHexOctet( + PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT].indexOf(language[1])); + }, + + /** + * Write Timer Value Comprehension TLV. + * + * @param seconds length of time during of the timer. + * @param cr Comprehension Required or not + */ + writeTimerValueTlv: function(seconds, cr) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TIMER_VALUE | + (cr ? COMPREHENSIONTLV_FLAG_CR : 0)); + GsmPDUHelper.writeHexOctet(3); + + // TS 102.223, clause 8.38 + // +----------------+------------------+-------------------+ + // | hours (1 byte) | minutes (1 btye) | seconds (1 byte) | + // +----------------+------------------+-------------------+ + GsmPDUHelper.writeSwappedNibbleBCDNum(Math.floor(seconds / 60 / 60)); + GsmPDUHelper.writeSwappedNibbleBCDNum(Math.floor(seconds / 60) % 60); + GsmPDUHelper.writeSwappedNibbleBCDNum(Math.floor(seconds) % 60); + }, + + writeTextStringTlv: function(text, coding) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let buf = GsmPDUHelper.writeWithBuffer(() => { + // Write Coding. + GsmPDUHelper.writeHexOctet(coding); + + // Write Text String. + switch (coding) { + case STK_TEXT_CODING_UCS2: + GsmPDUHelper.writeUCS2String(text); + break; + case STK_TEXT_CODING_GSM_7BIT_PACKED: + GsmPDUHelper.writeStringAsSeptets(text, 0, 0, 0); + break; + case STK_TEXT_CODING_GSM_8BIT: + GsmPDUHelper.writeStringAs8BitUnpacked(text); + break; + } + }); + + let length = buf.length; + if (length) { + // Write Tag. + GsmPDUHelper.writeHexOctet(COMPREHENSIONTLV_TAG_TEXT_STRING | + COMPREHENSIONTLV_FLAG_CR); + // Write Length. + this.writeLength(length); + // Write Value. + for (let i = 0; i < length; i++) { + GsmPDUHelper.writeHexOctet(buf[i]); + } + } + }, + + getSizeOfLengthOctets: function(length) { + if (length >= 0x10000) { + return 4; // 0x83, len_1, len_2, len_3 + } else if (length >= 0x100) { + return 3; // 0x82, len_1, len_2 + } else if (length >= 0x80) { + return 2; // 0x81, len + } else { + return 1; // len + } + }, + + writeLength: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + // TS 101.220 clause 7.1.2, Length Encoding. + // Length | Byte 1 | Byte 2 | Byte 3 | Byte 4 | + // 0 - 127 | 00 - 7f | N/A | N/A | N/A | + // 128-255 | 81 | 80 - ff| N/A | N/A | + // 256-65535| 82 | 0100 - ffff | N/A | + // 65536- | 83 | 010000 - ffffff | + // 16777215 + if (length < 0x80) { + GsmPDUHelper.writeHexOctet(length); + } else if (0x80 <= length && length < 0x100) { + GsmPDUHelper.writeHexOctet(0x81); + GsmPDUHelper.writeHexOctet(length); + } else if (0x100 <= length && length < 0x10000) { + GsmPDUHelper.writeHexOctet(0x82); + GsmPDUHelper.writeHexOctet((length >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(length & 0xff); + } else if (0x10000 <= length && length < 0x1000000) { + GsmPDUHelper.writeHexOctet(0x83); + GsmPDUHelper.writeHexOctet((length >> 16) & 0xff); + GsmPDUHelper.writeHexOctet((length >> 8) & 0xff); + GsmPDUHelper.writeHexOctet(length & 0xff); + } else { + throw new Error("Invalid length value :" + length); + } + }, +}; + +function BerTlvHelperObject(aContext) { + this.context = aContext; +} +BerTlvHelperObject.prototype = { + context: null, + + /** + * Decode Ber TLV. + * + * @param dataLen + * The length of data in bytes. + */ + decode: function(dataLen) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let hlen = 0; + let tag = GsmPDUHelper.readHexOctet(); + hlen++; + + // The length is coded onto 1 or 2 bytes. + // Length | Byte 1 | Byte 2 + // 0 - 127 | length ('00' to '7f') | N/A + // 128 - 255 | '81' | length ('80' to 'ff') + let length; + let temp = GsmPDUHelper.readHexOctet(); + hlen++; + if (temp < 0x80) { + length = temp; + } else if (temp === 0x81) { + length = GsmPDUHelper.readHexOctet(); + hlen++; + if (length < 0x80) { + throw new Error("Invalid length " + length); + } + } else { + throw new Error("Invalid length octet " + temp); + } + + // Header + body length check. + if (dataLen - hlen !== length) { + throw new Error("Unexpected BerTlvHelper value length!!"); + } + + let method = this[tag]; + if (typeof method != "function") { + throw new Error("Unknown Ber tag 0x" + tag.toString(16)); + } + + let value = method.call(this, length); + + return { + tag: tag, + length: length, + value: value + }; + }, + + /** + * Process the value part for FCP template TLV. + * + * @param length + * The length of data in bytes. + */ + processFcpTemplate: function(length) { + let tlvs = this.decodeChunks(length); + return tlvs; + }, + + /** + * Process the value part for proactive command TLV. + * + * @param length + * The length of data in bytes. + */ + processProactiveCommand: function(length) { + let ctlvs = this.context.ComprehensionTlvHelper.decodeChunks(length); + return ctlvs; + }, + + /** + * Decode raw data to a Ber-TLV. + */ + decodeInnerTlv: function() { + let GsmPDUHelper = this.context.GsmPDUHelper; + let tag = GsmPDUHelper.readHexOctet(); + let length = GsmPDUHelper.readHexOctet(); + return { + tag: tag, + length: length, + value: this.retrieve(tag, length) + }; + }, + + decodeChunks: function(length) { + let chunks = []; + let index = 0; + while (index < length) { + let tlv = this.decodeInnerTlv(); + if (tlv.value) { + chunks.push(tlv); + } + index += tlv.length; + // tag + length fields consume 2 bytes. + index += 2; + } + return chunks; + }, + + retrieve: function(tag, length) { + let method = this[tag]; + if (typeof method != "function") { + if (DEBUG) { + this.context.debug("Unknown Ber tag : 0x" + tag.toString(16)); + } + let Buf = this.context.Buf; + Buf.seekIncoming(length * Buf.PDU_HEX_OCTET_SIZE); + return null; + } + return method.call(this, length); + }, + + /** + * File Size Data. + * + * | Byte | Description | Length | + * | 1 | Tag | 1 | + * | 2 | Length | 1 | + * | 3 to X+24 | Number of allocated data bytes in the file | X | + * | | , excluding structural information | | + */ + retrieveFileSizeData: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let fileSizeData = 0; + for (let i = 0; i < length; i++) { + fileSizeData = fileSizeData << 8; + fileSizeData += GsmPDUHelper.readHexOctet(); + } + + return {fileSizeData: fileSizeData}; + }, + + /** + * File Descriptor. + * + * | Byte | Description | Length | + * | 1 | Tag | 1 | + * | 2 | Length | 1 | + * | 3 | File descriptor byte | 1 | + * | 4 | Data coding byte | 1 | + * | 5 ~ 6 | Record length | 2 | + * | 7 | Number of records | 1 | + */ + retrieveFileDescriptor: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + let fileDescriptorByte = GsmPDUHelper.readHexOctet(); + let dataCodingByte = GsmPDUHelper.readHexOctet(); + // See TS 102 221 Table 11.5, we only care the least 3 bits for the + // structure of file. + let fileStructure = fileDescriptorByte & 0x07; + + let fileDescriptor = { + fileStructure: fileStructure + }; + // byte 5 ~ 7 are mandatory for linear fixed and cyclic files, otherwise + // they are not applicable. + if (fileStructure === UICC_EF_STRUCTURE[EF_STRUCTURE_LINEAR_FIXED] || + fileStructure === UICC_EF_STRUCTURE[EF_STRUCTURE_CYCLIC]) { + fileDescriptor.recordLength = (GsmPDUHelper.readHexOctet() << 8) + + GsmPDUHelper.readHexOctet(); + fileDescriptor.numOfRecords = GsmPDUHelper.readHexOctet(); + } + + return fileDescriptor; + }, + + /** + * File identifier. + * + * | Byte | Description | Length | + * | 1 | Tag | 1 | + * | 2 | Length | 1 | + * | 3 ~ 4 | File identifier | 2 | + */ + retrieveFileIdentifier: function(length) { + let GsmPDUHelper = this.context.GsmPDUHelper; + return {fileId : (GsmPDUHelper.readHexOctet() << 8) + + GsmPDUHelper.readHexOctet()}; + }, + + searchForNextTag: function(tag, iter) { + for (let tlv of iter) { + if (tlv.tag === tag) { + return tlv; + } + } + return null; + } +}; +BerTlvHelperObject.prototype[BER_FCP_TEMPLATE_TAG] = function BER_FCP_TEMPLATE_TAG(length) { + return this.processFcpTemplate(length); +}; +BerTlvHelperObject.prototype[BER_PROACTIVE_COMMAND_TAG] = function BER_PROACTIVE_COMMAND_TAG(length) { + return this.processProactiveCommand(length); +}; +BerTlvHelperObject.prototype[BER_FCP_FILE_SIZE_DATA_TAG] = function BER_FCP_FILE_SIZE_DATA_TAG(length) { + return this.retrieveFileSizeData(length); +}; +BerTlvHelperObject.prototype[BER_FCP_FILE_DESCRIPTOR_TAG] = function BER_FCP_FILE_DESCRIPTOR_TAG(length) { + return this.retrieveFileDescriptor(length); +}; +BerTlvHelperObject.prototype[BER_FCP_FILE_IDENTIFIER_TAG] = function BER_FCP_FILE_IDENTIFIER_TAG(length) { + return this.retrieveFileIdentifier(length); +}; + +/** + * ICC Helper for getting EF path. + */ +function ICCFileHelperObject(aContext) { + this.context = aContext; +} +ICCFileHelperObject.prototype = { + context: null, + + /** + * This function handles only EFs that are common to RUIM, SIM, USIM + * and other types of ICC cards. + */ + getCommonEFPath: function(fileId) { + switch (fileId) { + case ICC_EF_ICCID: + return EF_PATH_MF_SIM; + case ICC_EF_ADN: + case ICC_EF_SDN: // Fall through. + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM; + case ICC_EF_PBR: + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_DF_PHONEBOOK; + case ICC_EF_IMG: + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_GRAPHICS; + } + return null; + }, + + /** + * This function handles EFs for SIM. + */ + getSimEFPath: function(fileId) { + switch (fileId) { + case ICC_EF_FDN: + case ICC_EF_MSISDN: + case ICC_EF_SMS: + case ICC_EF_EXT1: + case ICC_EF_EXT2: + case ICC_EF_EXT3: + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM; + case ICC_EF_AD: + case ICC_EF_MBDN: + case ICC_EF_MWIS: + case ICC_EF_PLMNsel: + case ICC_EF_SPN: + case ICC_EF_SPDI: + case ICC_EF_SST: + case ICC_EF_PHASE: + case ICC_EF_CBMI: + case ICC_EF_CBMID: + case ICC_EF_CBMIR: + case ICC_EF_OPL: + case ICC_EF_PNN: + case ICC_EF_GID1: + case ICC_EF_CPHS_INFO: + case ICC_EF_CPHS_MBN: + return EF_PATH_MF_SIM + EF_PATH_DF_GSM; + default: + return null; + } + }, + + /** + * This function handles EFs for USIM. + */ + getUSimEFPath: function(fileId) { + switch (fileId) { + case ICC_EF_AD: + case ICC_EF_FDN: + case ICC_EF_MBDN: + case ICC_EF_MWIS: + case ICC_EF_UST: + case ICC_EF_MSISDN: + case ICC_EF_SPN: + case ICC_EF_SPDI: + case ICC_EF_CBMI: + case ICC_EF_CBMID: + case ICC_EF_CBMIR: + case ICC_EF_OPL: + case ICC_EF_PNN: + case ICC_EF_SMS: + case ICC_EF_GID1: + // CPHS spec was provided in 1997 based on SIM requirement, there is no + // detailed info about how these ICC_EF_CPHS_XXX are allocated in USIM. + // What we can do now is to follow what has been done in AOSP to have file + // path equal to MF_SIM/DF_GSM. + case ICC_EF_CPHS_INFO: + case ICC_EF_CPHS_MBN: + return EF_PATH_MF_SIM + EF_PATH_ADF_USIM; + default: + // The file ids in USIM phone book entries are decided by the + // card manufacturer. So if we don't match any of the cases + // above and if its a USIM return the phone book path. + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_DF_PHONEBOOK; + } + }, + + /** + * This function handles EFs for RUIM + */ + getRuimEFPath: function(fileId) { + switch(fileId) { + case ICC_EF_CSIM_IMSI_M: + case ICC_EF_CSIM_CDMAHOME: + case ICC_EF_CSIM_CST: + case ICC_EF_CSIM_SPN: + return EF_PATH_MF_SIM + EF_PATH_DF_CDMA; + case ICC_EF_FDN: + case ICC_EF_EXT1: + case ICC_EF_EXT2: + case ICC_EF_EXT3: + return EF_PATH_MF_SIM + EF_PATH_DF_TELECOM; + default: + return null; + } + }, + + /** + * Helper function for getting the pathId for the specific ICC record + * depeding on which type of ICC card we are using. + * + * @param fileId + * File id. + * @return The pathId or null in case of an error or invalid input. + */ + getEFPath: function(fileId) { + let path = this.getCommonEFPath(fileId); + if (path) { + return path; + } + + switch (this.context.RIL.appType) { + case CARD_APPTYPE_SIM: + return this.getSimEFPath(fileId); + case CARD_APPTYPE_USIM: + return this.getUSimEFPath(fileId); + case CARD_APPTYPE_RUIM: + return this.getRuimEFPath(fileId); + default: + return null; + } + } +}; + +/** + * Helper for ICC IO functionalities. + */ +function ICCIOHelperObject(aContext) { + this.context = aContext; +} +ICCIOHelperObject.prototype = { + context: null, + + /** + * Load EF with type 'Linear Fixed'. + * + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + * @param recordNumber [optional] + * The number of the record shall be loaded. + * @param recordSize [optional] + * The size of the record. + * @param callback [optional] + * The callback function shall be called when the record(s) is read. + * @param onerror [optional] + * The callback function shall be called when failure. + */ + loadLinearFixedEF: function(options) { + let cb; + let readRecord = (function(options) { + options.command = ICC_COMMAND_READ_RECORD; + options.p1 = options.recordNumber || 1; // Record number + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = options.recordSize; + options.callback = cb || options.callback; + this.context.RIL.iccIO(options); + }).bind(this); + + options.structure = EF_STRUCTURE_LINEAR_FIXED; + options.pathId = this.context.ICCFileHelper.getEFPath(options.fileId); + if (options.recordSize) { + readRecord(options); + return; + } + + cb = options.callback; + options.callback = readRecord; + this.getResponse(options); + }, + + /** + * Load next record from current record Id. + */ + loadNextRecord: function(options) { + options.p1++; + this.context.RIL.iccIO(options); + }, + + /** + * Update EF with type 'Linear Fixed'. + * + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + * @param recordNumber + * The number of the record shall be updated. + * @param dataWriter [optional] + * The function for writing string parameter for the ICC_COMMAND_UPDATE_RECORD. + * @param pin2 [optional] + * PIN2 is required when updating ICC_EF_FDN. + * @param callback [optional] + * The callback function shall be called when the record is updated. + * @param onerror [optional] + * The callback function shall be called when failure. + */ + updateLinearFixedEF: function(options) { + if (!options.fileId || !options.recordNumber) { + throw new Error("Unexpected fileId " + options.fileId + + " or recordNumber " + options.recordNumber); + } + + options.structure = EF_STRUCTURE_LINEAR_FIXED; + options.pathId = this.context.ICCFileHelper.getEFPath(options.fileId); + let cb = options.callback; + options.callback = function callback(options) { + options.callback = cb; + options.command = ICC_COMMAND_UPDATE_RECORD; + options.p1 = options.recordNumber; + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = options.recordSize; + this.context.RIL.iccIO(options); + }.bind(this); + this.getResponse(options); + }, + + /** + * Load EF with type 'Transparent'. + * + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + * @param callback [optional] + * The callback function shall be called when the record(s) is read. + * @param onerror [optional] + * The callback function shall be called when failure. + */ + loadTransparentEF: function(options) { + options.structure = EF_STRUCTURE_TRANSPARENT; + let cb = options.callback; + options.callback = function callback(options) { + options.callback = cb; + options.command = ICC_COMMAND_READ_BINARY; + options.p2 = 0x00; + options.p3 = options.fileSize; + this.context.RIL.iccIO(options); + }.bind(this); + this.getResponse(options); + }, + + /** + * Use ICC_COMMAND_GET_RESPONSE to query the EF. + * + * @param fileId + * The file to operate on, one of the ICC_EF_* constants. + */ + getResponse: function(options) { + options.command = ICC_COMMAND_GET_RESPONSE; + options.pathId = options.pathId || + this.context.ICCFileHelper.getEFPath(options.fileId); + if (!options.pathId) { + throw new Error("Unknown pathId for " + options.fileId.toString(16)); + } + options.p1 = 0; // For GET_RESPONSE, p1 = 0 + switch (this.context.RIL.appType) { + case CARD_APPTYPE_USIM: + options.p2 = GET_RESPONSE_FCP_TEMPLATE; + options.p3 = 0x00; + break; + // For RUIM, CSIM and ISIM, cf bug 955946: keep the old behavior + case CARD_APPTYPE_RUIM: + case CARD_APPTYPE_CSIM: + case CARD_APPTYPE_ISIM: + // For SIM, this is what we want + case CARD_APPTYPE_SIM: + default: + options.p2 = 0x00; + options.p3 = GET_RESPONSE_EF_SIZE_BYTES; + break; + } + this.context.RIL.iccIO(options); + }, + + /** + * Process ICC I/O response. + */ + processICCIO: function(options) { + let func = this[options.command]; + func.call(this, options); + }, + + /** + * Process a ICC_COMMAND_GET_RESPONSE type command for REQUEST_SIM_IO. + */ + processICCIOGetResponse: function(options) { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + + let peek = this.context.GsmPDUHelper.readHexOctet(); + Buf.seekIncoming(-1 * Buf.PDU_HEX_OCTET_SIZE); + if (peek === BER_FCP_TEMPLATE_TAG) { + this.processUSimGetResponse(options, strLen / 2); + } else { + this.processSimGetResponse(options); + } + Buf.readStringDelimiter(strLen); + + if (options.callback) { + options.callback(options); + } + }, + + /** + * Helper function for processing USIM get response. + */ + processUSimGetResponse: function(options, octetLen) { + let BerTlvHelper = this.context.BerTlvHelper; + + let berTlv = BerTlvHelper.decode(octetLen); + // See TS 102 221 Table 11.4 for the content order of getResponse. + let iter = berTlv.value.values(); + let tlv = BerTlvHelper.searchForNextTag(BER_FCP_FILE_DESCRIPTOR_TAG, + iter); + if (!tlv || + (tlv.value.fileStructure !== UICC_EF_STRUCTURE[options.structure])) { + throw new Error("Expected EF structure " + + UICC_EF_STRUCTURE[options.structure] + + " but read " + tlv.value.fileStructure); + } + + if (tlv.value.fileStructure === UICC_EF_STRUCTURE[EF_STRUCTURE_LINEAR_FIXED] || + tlv.value.fileStructure === UICC_EF_STRUCTURE[EF_STRUCTURE_CYCLIC]) { + options.recordSize = tlv.value.recordLength; + options.totalRecords = tlv.value.numOfRecords; + } + + tlv = BerTlvHelper.searchForNextTag(BER_FCP_FILE_IDENTIFIER_TAG, iter); + if (!tlv || (tlv.value.fileId !== options.fileId)) { + throw new Error("Expected file ID " + options.fileId.toString(16) + + " but read " + fileId.toString(16)); + } + + tlv = BerTlvHelper.searchForNextTag(BER_FCP_FILE_SIZE_DATA_TAG, iter); + if (!tlv) { + throw new Error("Unexpected file size data"); + } + options.fileSize = tlv.value.fileSizeData; + }, + + /** + * Helper function for processing SIM get response. + */ + processSimGetResponse: function(options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + // The format is from TS 51.011, clause 9.2.1 + + // Skip RFU, data[0] data[1]. + Buf.seekIncoming(2 * Buf.PDU_HEX_OCTET_SIZE); + + // File size, data[2], data[3] + options.fileSize = (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(); + + // 2 bytes File id. data[4], data[5] + let fileId = (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(); + if (fileId != options.fileId) { + throw new Error("Expected file ID " + options.fileId.toString(16) + + " but read " + fileId.toString(16)); + } + + // Type of file, data[6] + let fileType = GsmPDUHelper.readHexOctet(); + if (fileType != TYPE_EF) { + throw new Error("Unexpected file type " + fileType); + } + + // Skip 1 byte RFU, data[7], + // 3 bytes Access conditions, data[8] data[9] data[10], + // 1 byte File status, data[11], + // 1 byte Length of the following data, data[12]. + Buf.seekIncoming(((RESPONSE_DATA_STRUCTURE - RESPONSE_DATA_FILE_TYPE - 1) * + Buf.PDU_HEX_OCTET_SIZE)); + + // Read Structure of EF, data[13] + let efStructure = GsmPDUHelper.readHexOctet(); + if (efStructure != options.structure) { + throw new Error("Expected EF structure " + options.structure + + " but read " + efStructure); + } + + // Length of a record, data[14]. + // Only available for LINEAR_FIXED and CYCLIC. + if (efStructure == EF_STRUCTURE_LINEAR_FIXED || + efStructure == EF_STRUCTURE_CYCLIC) { + options.recordSize = GsmPDUHelper.readHexOctet(); + options.totalRecords = options.fileSize / options.recordSize; + } else { + Buf.seekIncoming(1 * Buf.PDU_HEX_OCTET_SIZE); + } + }, + + /** + * Process a ICC_COMMAND_READ_RECORD type command for REQUEST_SIM_IO. + */ + processICCIOReadRecord: function(options) { + if (options.callback) { + options.callback(options); + } + }, + + /** + * Process a ICC_COMMAND_READ_BINARY type command for REQUEST_SIM_IO. + */ + processICCIOReadBinary: function(options) { + if (options.callback) { + options.callback(options); + } + }, + + /** + * Process a ICC_COMMAND_UPDATE_RECORD type command for REQUEST_SIM_IO. + */ + processICCIOUpdateRecord: function(options) { + if (options.callback) { + options.callback(options); + } + }, +}; +ICCIOHelperObject.prototype[ICC_COMMAND_SEEK] = null; +ICCIOHelperObject.prototype[ICC_COMMAND_READ_BINARY] = function ICC_COMMAND_READ_BINARY(options) { + this.processICCIOReadBinary(options); +}; +ICCIOHelperObject.prototype[ICC_COMMAND_READ_RECORD] = function ICC_COMMAND_READ_RECORD(options) { + this.processICCIOReadRecord(options); +}; +ICCIOHelperObject.prototype[ICC_COMMAND_GET_RESPONSE] = function ICC_COMMAND_GET_RESPONSE(options) { + this.processICCIOGetResponse(options); +}; +ICCIOHelperObject.prototype[ICC_COMMAND_UPDATE_BINARY] = null; +ICCIOHelperObject.prototype[ICC_COMMAND_UPDATE_RECORD] = function ICC_COMMAND_UPDATE_RECORD(options) { + this.processICCIOUpdateRecord(options); +}; + +/** + * Helper for ICC records. + */ +function ICCRecordHelperObject(aContext) { + this.context = aContext; + // Cache the possible free record id for all files, use fileId as key. + this._freeRecordIds = {}; +} +ICCRecordHelperObject.prototype = { + context: null, + + /** + * Fetch ICC records. + */ + fetchICCRecords: function() { + switch (this.context.RIL.appType) { + case CARD_APPTYPE_SIM: + case CARD_APPTYPE_USIM: + this.context.SimRecordHelper.fetchSimRecords(); + break; + case CARD_APPTYPE_RUIM: + this.context.RuimRecordHelper.fetchRuimRecords(); + break; + } + }, + + /** + * Read the ICCID. + */ + readICCID: function() { + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + RIL.iccInfo.iccid = + GsmPDUHelper.readSwappedNibbleBcdString(octetLen, true); + // Consumes the remaining buffer if any. + let unReadBuffer = this.context.Buf.getReadAvailable() - + this.context.Buf.PDU_HEX_OCTET_SIZE; + if (unReadBuffer > 0) { + this.context.Buf.seekIncoming(unReadBuffer); + } + Buf.readStringDelimiter(strLen); + + if (DEBUG) this.context.debug("ICCID: " + RIL.iccInfo.iccid); + if (RIL.iccInfo.iccid) { + this.context.ICCUtilsHelper.handleICCInfoChange(); + RIL.reportStkServiceIsRunning(); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_ICCID, + callback: callback.bind(this) + }); + }, + + /** + * Read ICC ADN like EF, i.e. EF_ADN, EF_FDN. + * + * @param fileId EF id of the ADN, FDN or SDN. + * @param extFileId EF id of the EXT. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readADNLike: function(fileId, extFileId, onsuccess, onerror) { + let ICCIOHelper = this.context.ICCIOHelper; + + function callback(options) { + let loadNextContactRecord = () => { + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + return; + } + if (DEBUG) { + for (let i = 0; i < contacts.length; i++) { + this.context.debug("contact [" + i + "] " + + JSON.stringify(contacts[i])); + } + } + if (onsuccess) { + onsuccess(contacts); + } + }; + + let contact = + this.context.ICCPDUHelper.readAlphaIdDiallingNumber(options.recordSize); + if (contact) { + let record = { + recordId: options.p1, + alphaId: contact.alphaId, + number: contact.number + }; + contacts.push(record); + + if (extFileId && contact.extRecordNumber != 0xff) { + this.readExtension(extFileId, contact.extRecordNumber, (number) => { + if (number) { + record.number += number; + } + loadNextContactRecord(); + }, () => loadNextContactRecord()); + return; + } + } + loadNextContactRecord(); + } + + let contacts = []; + ICCIOHelper.loadLinearFixedEF({fileId: fileId, + callback: callback.bind(this), + onerror: onerror}); + }, + + /** + * Update ICC ADN like EFs, like EF_ADN, EF_FDN. + * + * @param fileId EF id of the ADN or FDN. + * @param extRecordNumber The record identifier of the EXT. + * @param contact The contact will be updated. (Shall have recordId property) + * @param pin2 PIN2 is required when updating ICC_EF_FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateADNLike: function(fileId, extRecordNumber, contact, pin2, onsuccess, onerror) { + let updatedContact; + function dataWriter(recordSize) { + updatedContact = this.context.ICCPDUHelper.writeAlphaIdDiallingNumber(recordSize, + contact.alphaId, + contact.number, + extRecordNumber); + } + + function callback(options) { + if (onsuccess) { + onsuccess(updatedContact); + } + } + + if (!contact || !contact.recordId) { + if (onerror) onerror(GECKO_ERROR_INVALID_PARAMETER); + return; + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: contact.recordId, + dataWriter: dataWriter.bind(this), + pin2: pin2, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Read USIM/RUIM Phonebook. + * + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readPBR: function(onsuccess, onerror) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + let ICCIOHelper = this.context.ICCIOHelper; + let ICCUtilsHelper = this.context.ICCUtilsHelper; + let RIL = this.context.RIL; + + function callback(options) { + let strLen = Buf.readInt32(); + let octetLen = strLen / 2, readLen = 0; + + let pbrTlvs = []; + while (readLen < octetLen) { + let tag = GsmPDUHelper.readHexOctet(); + if (tag == 0xff) { + readLen++; + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + break; + } + + let tlvLen = GsmPDUHelper.readHexOctet(); + let tlvs = ICCUtilsHelper.decodeSimTlvs(tlvLen); + pbrTlvs.push({tag: tag, + length: tlvLen, + value: tlvs}); + + readLen += tlvLen + 2; // +2 for tag and tlvLen + } + Buf.readStringDelimiter(strLen); + + if (pbrTlvs.length > 0) { + let pbr = ICCUtilsHelper.parsePbrTlvs(pbrTlvs); + // EF_ADN is mandatory if and only if DF_PHONEBOOK is present. + if (!pbr.adn) { + if (onerror) onerror("Cannot access ADN."); + return; + } + pbrs.push(pbr); + } + + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + } else { + if (onsuccess) { + RIL.iccInfoPrivate.pbrs = pbrs; + onsuccess(pbrs); + } + } + } + + if (RIL.iccInfoPrivate.pbrs) { + onsuccess(RIL.iccInfoPrivate.pbrs); + return; + } + + let pbrs = []; + ICCIOHelper.loadLinearFixedEF({fileId : ICC_EF_PBR, + callback: callback.bind(this), + onerror: onerror}); + }, + + /** + * Cache EF_IAP record size. + */ + _iapRecordSize: null, + + /** + * Read ICC EF_IAP. (Index Administration Phonebook) + * + * @see TS 131.102, clause 4.4.2.2 + * + * @param fileId EF id of the IAP. + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readIAP: function(fileId, recordNumber, onsuccess, onerror) { + function callback(options) { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + this._iapRecordSize = options.recordSize; + + let iap = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + + if (onsuccess) { + onsuccess(iap); + } + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + recordSize: this._iapRecordSize, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Update USIM/RUIM Phonebook EF_IAP. + * + * @see TS 131.102, clause 4.4.2.13 + * + * @param fileId EF id of the IAP. + * @param recordNumber The identifier of the record shall be updated. + * @param iap The IAP value to be written. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateIAP: function(fileId, recordNumber, iap, onsuccess, onerror) { + let dataWriter = function dataWriter(recordSize) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + // Write String length + let strLen = recordSize * 2; + Buf.writeInt32(strLen); + + for (let i = 0; i < iap.length; i++) { + GsmPDUHelper.writeHexOctet(iap[i]); + } + + Buf.writeStringDelimiter(strLen); + }.bind(this); + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: onsuccess, + onerror: onerror + }); + }, + + /** + * Cache EF_Email record size. + */ + _emailRecordSize: null, + + /** + * Read USIM/RUIM Phonebook EF_EMAIL. + * + * @see TS 131.102, clause 4.4.2.13 + * + * @param fileId EF id of the EMAIL. + * @param fileType The type of the EMAIL, one of the ICC_USIM_TYPE* constants. + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readEmail: function(fileId, fileType, recordNumber, onsuccess, onerror) { + function callback(options) { + let Buf = this.context.Buf; + let ICCPDUHelper = this.context.ICCPDUHelper; + + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + let email = null; + this._emailRecordSize = options.recordSize; + + // Read contact's email + // + // | Byte | Description | Length | M/O + // | 1 ~ X | E-mail Address | X | M + // | X+1 | ADN file SFI | 1 | C + // | X+2 | ADN file Record Identifier | 1 | C + // Note: The fields marked as C above are mandatort if the file + // is not type 1 (as specified in EF_PBR) + if (fileType == ICC_USIM_TYPE1_TAG) { + email = ICCPDUHelper.read8BitUnpackedToString(octetLen); + } else { + email = ICCPDUHelper.read8BitUnpackedToString(octetLen - 2); + + // Consumes the remaining buffer + Buf.seekIncoming(2 * Buf.PDU_HEX_OCTET_SIZE); // For ADN SFI and Record Identifier + } + + Buf.readStringDelimiter(strLen); + + if (onsuccess) { + onsuccess(email); + } + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + recordSize: this._emailRecordSize, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Update USIM/RUIM Phonebook EF_EMAIL. + * + * @see TS 131.102, clause 4.4.2.13 + * + * @param pbr Phonebook Reference File. + * @param recordNumber The identifier of the record shall be updated. + * @param email The value to be written. + * @param adnRecordId The record Id of ADN, only needed if the fileType of Email is TYPE2. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateEmail: function(pbr, recordNumber, email, adnRecordId, onsuccess, onerror) { + let fileId = pbr[USIM_PBR_EMAIL].fileId; + let fileType = pbr[USIM_PBR_EMAIL].fileType; + let writtenEmail; + let dataWriter = function dataWriter(recordSize) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + let ICCPDUHelper = this.context.ICCPDUHelper; + + // Write String length + let strLen = recordSize * 2; + Buf.writeInt32(strLen); + + if (fileType == ICC_USIM_TYPE1_TAG) { + writtenEmail = ICCPDUHelper.writeStringTo8BitUnpacked(recordSize, email); + } else { + writtenEmail = ICCPDUHelper.writeStringTo8BitUnpacked(recordSize - 2, email); + GsmPDUHelper.writeHexOctet(pbr.adn.sfi || 0xff); + GsmPDUHelper.writeHexOctet(adnRecordId); + } + + Buf.writeStringDelimiter(strLen); + }.bind(this); + + let callback = (options) => { + if (onsuccess) { + onsuccess(writtenEmail); + } + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: callback, + onerror: onerror + }); + }, + + /** + * Cache EF_ANR record size. + */ + _anrRecordSize: null, + + /** + * Read USIM/RUIM Phonebook EF_ANR. + * + * @see TS 131.102, clause 4.4.2.9 + * + * @param fileId EF id of the ANR. + * @param fileType One of the ICC_USIM_TYPE* constants. + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readANR: function(fileId, fileType, recordNumber, onsuccess, onerror) { + function callback(options) { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let number = null; + this._anrRecordSize = options.recordSize; + + // Skip EF_AAS Record ID. + Buf.seekIncoming(1 * Buf.PDU_HEX_OCTET_SIZE); + + number = this.context.ICCPDUHelper.readNumberWithLength(); + + // Skip 2 unused octets, CCP and EXT1. + Buf.seekIncoming(2 * Buf.PDU_HEX_OCTET_SIZE); + + // For Type 2 there are two extra octets. + if (fileType == ICC_USIM_TYPE2_TAG) { + // Skip 2 unused octets, ADN SFI and Record Identifier. + Buf.seekIncoming(2 * Buf.PDU_HEX_OCTET_SIZE); + } + + Buf.readStringDelimiter(strLen); + + if (onsuccess) { + onsuccess(number); + } + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + recordSize: this._anrRecordSize, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Update USIM/RUIM Phonebook EF_ANR. + * + * @see TS 131.102, clause 4.4.2.9 + * + * @param pbr Phonebook Reference File. + * @param recordNumber The identifier of the record shall be updated. + * @param number The value to be written. + * @param adnRecordId The record Id of ADN, only needed if the fileType of Email is TYPE2. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateANR: function(pbr, recordNumber, number, adnRecordId, onsuccess, onerror) { + let fileId = pbr[USIM_PBR_ANR0].fileId; + let fileType = pbr[USIM_PBR_ANR0].fileType; + let writtenNumber; + let dataWriter = function dataWriter(recordSize) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + // Write String length + let strLen = recordSize * 2; + Buf.writeInt32(strLen); + + // EF_AAS record Id. Unused for now. + GsmPDUHelper.writeHexOctet(0xff); + + writtenNumber = this.context.ICCPDUHelper.writeNumberWithLength(number); + + // Write unused octets 0xff, CCP and EXT1. + GsmPDUHelper.writeHexOctet(0xff); + GsmPDUHelper.writeHexOctet(0xff); + + // For Type 2 there are two extra octets. + if (fileType == ICC_USIM_TYPE2_TAG) { + GsmPDUHelper.writeHexOctet(pbr.adn.sfi || 0xff); + GsmPDUHelper.writeHexOctet(adnRecordId); + } + + Buf.writeStringDelimiter(strLen); + }.bind(this); + + let callback = (options) => { + if (onsuccess) { + onsuccess(writtenNumber); + } + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: callback, + onerror: onerror + }); + }, + + /** + * Cache the possible free record id for all files. + */ + _freeRecordIds: null, + + /** + * Find free record id. + * + * @param fileId EF id. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + findFreeRecordId: function(fileId, onsuccess, onerror) { + let ICCIOHelper = this.context.ICCIOHelper; + + function callback(options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + let readLen = 0; + + while (readLen < octetLen) { + let octet = GsmPDUHelper.readHexOctet(); + readLen++; + if (octet != 0xff) { + break; + } + } + + let nextRecord = (options.p1 % options.totalRecords) + 1; + + if (readLen == octetLen) { + // Find free record, assume next record is probably free. + this._freeRecordIds[fileId] = nextRecord; + if (onsuccess) { + onsuccess(options.p1); + } + return; + } else { + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + } + + Buf.readStringDelimiter(strLen); + + if (nextRecord !== recordNumber) { + options.p1 = nextRecord; + this.context.RIL.iccIO(options); + } else { + // No free record found. + delete this._freeRecordIds[fileId]; + if (DEBUG) { + this.context.debug(CONTACT_ERR_NO_FREE_RECORD_FOUND); + } + onerror(CONTACT_ERR_NO_FREE_RECORD_FOUND); + } + } + + // Start searching free records from the possible one. + let recordNumber = this._freeRecordIds[fileId] || 1; + ICCIOHelper.loadLinearFixedEF({fileId: fileId, + recordNumber: recordNumber, + callback: callback.bind(this), + onerror: onerror}); + }, + + /** + * Read Extension Number from TS 151.011 clause 10.5.10, TS 31.102, clause 4.4.2.4 + * + * @param fileId EF Extension id + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readExtension: function(fileId, recordNumber, onsuccess, onerror) { + let callback = (options) => { + let Buf = this.context.Buf; + let length = Buf.readInt32(); + let recordType = this.context.GsmPDUHelper.readHexOctet(); + let number = ""; + + // TS 31.102, clause 4.4.2.4 EFEXT1 + // Case 1, Extension1 record is additional data + if (recordType & 0x02) { + let numLen = this.context.GsmPDUHelper.readHexOctet(); + if (numLen != 0xff) { + if (numLen > EXT_MAX_BCD_NUMBER_BYTES) { + if (DEBUG) { + this.context.debug( + "Error: invalid length of BCD number/SSC contents - " + numLen); + } + // +1 to skip Identifier + Buf.seekIncoming((EXT_MAX_BCD_NUMBER_BYTES + 1) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(length); + onerror(); + return; + } + + number = this.context.GsmPDUHelper.readSwappedNibbleExtendedBcdString(numLen); + if (DEBUG) this.context.debug("Contact Extension Number: "+ number); + Buf.seekIncoming((EXT_MAX_BCD_NUMBER_BYTES - numLen) * Buf.PDU_HEX_OCTET_SIZE); + } else { + Buf.seekIncoming(EXT_MAX_BCD_NUMBER_BYTES * Buf.PDU_HEX_OCTET_SIZE); + } + } else { + // Don't support Case 2, Extension1 record is Called Party Subaddress. + // +1 skip numLen + Buf.seekIncoming((EXT_MAX_BCD_NUMBER_BYTES + 1) * Buf.PDU_HEX_OCTET_SIZE); + } + + // Skip Identifier + Buf.seekIncoming(Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(length); + onsuccess(number); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + callback: callback, + onerror: onerror + }); + }, + + /** + * Update Extension. + * + * @param fileId EF id of the EXT. + * @param recordNumber The number of the record shall be updated. + * @param number Dialling Number to be written. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateExtension: function(fileId, recordNumber, number, onsuccess, onerror) { + let dataWriter = (recordSize) => { + let GsmPDUHelper = this.context.GsmPDUHelper; + // Write String length + let strLen = recordSize * 2; + let Buf = this.context.Buf; + Buf.writeInt32(strLen); + + // We don't support extension chain. + if (number.length > EXT_MAX_NUMBER_DIGITS) { + number = number.substring(0, EXT_MAX_NUMBER_DIGITS); + } + + let numLen = Math.ceil(number.length / 2); + // Write Extension record + GsmPDUHelper.writeHexOctet(0x02); + GsmPDUHelper.writeHexOctet(numLen); + GsmPDUHelper.writeSwappedNibbleBCD(number); + // Write trailing 0xff of Extension data. + for (let i = 0; i < EXT_MAX_BCD_NUMBER_BYTES - numLen; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + // Write trailing 0xff for Identifier. + GsmPDUHelper.writeHexOctet(0xff); + Buf.writeStringDelimiter(strLen); + }; + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: onsuccess, + onerror: onerror + }); + }, + + /** + * Clean an EF record. + * + * @param fileId EF id. + * @param recordNumber The number of the record shall be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + cleanEFRecord: function(fileId, recordNumber, onsuccess, onerror) { + let dataWriter = (recordSize) => { + let GsmPDUHelper = this.context.GsmPDUHelper; + let Buf = this.context.Buf; + // Write String length + let strLen = recordSize * 2; + + Buf.writeInt32(strLen); + // Write record to 0xff + for (let i = 0; i < recordSize; i++) { + GsmPDUHelper.writeHexOctet(0xff); + } + Buf.writeStringDelimiter(strLen); + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + dataWriter: dataWriter, + callback: onsuccess, + onerror: onerror + }); + }, + + /** + * Get ADNLike extension record number. + * + * @param fileId EF id of the ADN or FDN. + * @param recordNumber EF record id of the ADN or FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + getADNLikeExtensionRecordNumber: function(fileId, recordNumber, onsuccess, onerror) { + let callback = (options) => { + let Buf = this.context.Buf; + let length = Buf.readInt32(); + + // Skip alphaLen, numLen, BCD Number, CCP octets. + Buf.seekIncoming((options.recordSize -1) * Buf.PDU_HEX_OCTET_SIZE); + + let extRecordNumber = this.context.GsmPDUHelper.readHexOctet(); + Buf.readStringDelimiter(length); + + onsuccess(extRecordNumber); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: fileId, + recordNumber: recordNumber, + callback: callback, + onerror: onerror + }); + }, +}; + +/** + * Helper for (U)SIM Records. + */ +function SimRecordHelperObject(aContext) { + this.context = aContext; +} +SimRecordHelperObject.prototype = { + context: null, + + /** + * Fetch (U)SIM records. + */ + fetchSimRecords: function() { + this.context.RIL.getIMSI(); + this.readAD(); + // CPHS was widely introduced in Europe during GSM(2G) era to provide easier + // access to carrier's core service like voicemail, call forwarding, manual + // PLMN selection, and etc. + // Addition EF like EF_CPHS_MBN, EF_CPHS_CPHS_CFF, EF_CPHS_VWI, etc are + // introduced to support these feature. + // In USIM, the replancement of these EFs are provided. (EF_MBDN, EF_MWIS, ...) + // However, some carriers in Europe still rely on these EFs. + this.readCphsInfo(() => this.readSST(), + (aErrorMsg) => { + this.context.debug("Failed to read CPHS_INFO: " + aErrorMsg); + this.readSST(); + }); + }, + + /** + * Read EF_phase. + * This EF is only available in SIM. + */ + readSimPhase: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + + let GsmPDUHelper = this.context.GsmPDUHelper; + let phase = GsmPDUHelper.readHexOctet(); + // If EF_phase is coded '03' or greater, an ME supporting STK shall + // perform the PROFILE DOWNLOAD procedure. + if (RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD && + phase >= ICC_PHASE_2_PROFILE_DOWNLOAD_REQUIRED) { + this.context.RIL.sendStkTerminalProfile(STK_SUPPORTED_TERMINAL_PROFILE); + } + + Buf.readStringDelimiter(strLen); + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_PHASE, + callback: callback.bind(this) + }); + }, + + /** + * Read the MSISDN from the (U)SIM. + */ + readMSISDN: function() { + function callback(options) { + let RIL = this.context.RIL; + + let contact = + this.context.ICCPDUHelper.readAlphaIdDiallingNumber(options.recordSize); + if (!contact || + (RIL.iccInfo.msisdn !== undefined && + RIL.iccInfo.msisdn === contact.number)) { + return; + } + RIL.iccInfo.msisdn = contact.number; + if (DEBUG) this.context.debug("MSISDN: " + RIL.iccInfo.msisdn); + this.context.ICCUtilsHelper.handleICCInfoChange(); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_MSISDN, + callback: callback.bind(this) + }); + }, + + /** + * Read the AD (Administrative Data) from the (U)SIM. + */ + readAD: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let ad = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + + if (DEBUG) { + let str = ""; + for (let i = 0; i < ad.length; i++) { + str += ad[i] + ", "; + } + this.context.debug("AD: " + str); + } + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + let RIL = this.context.RIL; + // TS 31.102, clause 4.2.18 EFAD + let mncLength = 0; + if (ad && ad[3]) { + mncLength = ad[3] & 0x0f; + if (mncLength != 0x02 && mncLength != 0x03) { + mncLength = 0; + } + } + // The 4th byte of the response is the length of MNC. + let mccMnc = ICCUtilsHelper.parseMccMncFromImsi(RIL.iccInfoPrivate.imsi, + mncLength); + if (mccMnc) { + RIL.iccInfo.mcc = mccMnc.mcc; + RIL.iccInfo.mnc = mccMnc.mnc; + ICCUtilsHelper.handleICCInfoChange(); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_AD, + callback: callback.bind(this) + }); + }, + + /** + * Read the SPN (Service Provider Name) from the (U)SIM. + */ + readSPN: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let spnDisplayCondition = this.context.GsmPDUHelper.readHexOctet(); + // Minus 1 because the first octet is used to store display condition. + let spn = this.context.ICCPDUHelper.readAlphaIdentifier(octetLen - 1); + Buf.readStringDelimiter(strLen); + + if (DEBUG) { + this.context.debug("SPN: spn = " + spn + + ", spnDisplayCondition = " + spnDisplayCondition); + } + + let RIL = this.context.RIL; + RIL.iccInfoPrivate.spnDisplayCondition = spnDisplayCondition; + RIL.iccInfo.spn = spn; + let ICCUtilsHelper = this.context.ICCUtilsHelper; + ICCUtilsHelper.updateDisplayCondition(); + ICCUtilsHelper.handleICCInfoChange(); + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_SPN, + callback: callback.bind(this) + }); + }, + + readIMG: function(recordNumber, onsuccess, onerror) { + function callback(options) { + let RIL = this.context.RIL; + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + + let numInstances = GsmPDUHelper.readHexOctet(); + + // Data length is defined as 9n+1 or 9n+2. See TS 31.102, sub-clause + // 4.6.1.1. However, it's likely to have padding appended so we have a + // rather loose check. + if (octetLen < (9 * numInstances + 1)) { + Buf.seekIncoming((octetLen - 1) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + if (onerror) { + onerror(); + } + return; + } + + let imgDescriptors = []; + for (let i = 0; i < numInstances; i++) { + imgDescriptors[i] = { + width: GsmPDUHelper.readHexOctet(), + height: GsmPDUHelper.readHexOctet(), + codingScheme: GsmPDUHelper.readHexOctet(), + fileId: (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(), + offset: (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(), + dataLen: (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet() + }; + } + Buf.seekIncoming((octetLen - 9 * numInstances - 1) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + + let instances = []; + let currentInstance = 0; + let readNextInstance = (function(img) { + instances[currentInstance] = img; + currentInstance++; + + if (currentInstance < numInstances) { + let imgDescriptor = imgDescriptors[currentInstance]; + this.readIIDF(imgDescriptor.fileId, + imgDescriptor.offset, + imgDescriptor.dataLen, + imgDescriptor.codingScheme, + readNextInstance, + onerror); + } else { + if (onsuccess) { + onsuccess(instances); + } + } + }).bind(this); + + this.readIIDF(imgDescriptors[0].fileId, + imgDescriptors[0].offset, + imgDescriptors[0].dataLen, + imgDescriptors[0].codingScheme, + readNextInstance, + onerror); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_IMG, + recordNumber: recordNumber, + callback: callback.bind(this), + onerror: onerror + }); + }, + + readIIDF: function(fileId, offset, dataLen, codingScheme, onsuccess, onerror) { + // Valid fileId is '4FXX', see TS 31.102, clause 4.6.1.2. + if ((fileId >> 8) != 0x4F) { + if (onerror) { + onerror(); + } + return; + } + + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + let GsmPDUHelper = this.context.GsmPDUHelper; + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + + if (octetLen < offset + dataLen) { + // Data length is not enough. See TS 31.102, clause 4.6.1.1, the + // paragraph "Bytes 8 and 9: Length of Image Instance Data." + Buf.seekIncoming(octetLen * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + if (onerror) { + onerror(); + } + return; + } + + Buf.seekIncoming(offset * Buf.PDU_HEX_OCTET_SIZE); + + let rawData = { + width: GsmPDUHelper.readHexOctet(), + height: GsmPDUHelper.readHexOctet(), + codingScheme: codingScheme + }; + + switch (codingScheme) { + case ICC_IMG_CODING_SCHEME_BASIC: + rawData.body = GsmPDUHelper.readHexOctetArray( + dataLen - ICC_IMG_HEADER_SIZE_BASIC); + Buf.seekIncoming((octetLen - offset - dataLen) * Buf.PDU_HEX_OCTET_SIZE); + break; + + case ICC_IMG_CODING_SCHEME_COLOR: + case ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY: + rawData.bitsPerImgPoint = GsmPDUHelper.readHexOctet(); + let num = GsmPDUHelper.readHexOctet(); + // The value 0 shall be interpreted as 256. See TS 31.102, Annex B.2. + rawData.numOfClutEntries = (num === 0) ? 0x100 : num; + rawData.clutOffset = (GsmPDUHelper.readHexOctet() << 8) | + GsmPDUHelper.readHexOctet(); + rawData.body = GsmPDUHelper.readHexOctetArray( + dataLen - ICC_IMG_HEADER_SIZE_COLOR); + + Buf.seekIncoming((rawData.clutOffset - offset - dataLen) * + Buf.PDU_HEX_OCTET_SIZE); + let clut = GsmPDUHelper.readHexOctetArray(rawData.numOfClutEntries * + ICC_CLUT_ENTRY_SIZE); + + rawData.clut = clut; + } + + Buf.readStringDelimiter(strLen); + + if (onsuccess) { + onsuccess(rawData); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: fileId, + pathId: this.context.ICCFileHelper.getEFPath(ICC_EF_IMG), + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Read the (U)SIM Service Table from the (U)SIM. + */ + readSST: function() { + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let sst = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + RIL.iccInfoPrivate.sst = sst; + if (DEBUG) { + let str = ""; + for (let i = 0; i < sst.length; i++) { + str += sst[i] + ", "; + } + this.context.debug("SST: " + str); + } + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + if (ICCUtilsHelper.isICCServiceAvailable("MSISDN")) { + if (DEBUG) this.context.debug("MSISDN: MSISDN is available"); + this.readMSISDN(); + } else { + if (DEBUG) this.context.debug("MSISDN: MSISDN service is not available"); + } + + // Fetch SPN and PLMN list, if some of them are available. + if (ICCUtilsHelper.isICCServiceAvailable("SPN")) { + if (DEBUG) this.context.debug("SPN: SPN is available"); + this.readSPN(); + } else { + if (DEBUG) this.context.debug("SPN: SPN service is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("MDN")) { + if (DEBUG) this.context.debug("MDN: MDN available."); + this.readMBDN(); + } else { + if (DEBUG) this.context.debug("MDN: MDN service is not available"); + + if (ICCUtilsHelper.isCphsServiceAvailable("MBN")) { + // read CPHS_MBN in advance if MBDN is not available. + this.readCphsMBN(); + } else { + if (DEBUG) this.context.debug("CPHS_MBN: CPHS_MBN service is not available"); + } + } + + if (ICCUtilsHelper.isICCServiceAvailable("MWIS")) { + if (DEBUG) this.context.debug("MWIS: MWIS is available"); + this.readMWIS(); + } else { + if (DEBUG) this.context.debug("MWIS: MWIS is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("SPDI")) { + if (DEBUG) this.context.debug("SPDI: SPDI available."); + this.readSPDI(); + } else { + if (DEBUG) this.context.debug("SPDI: SPDI service is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("PNN")) { + if (DEBUG) this.context.debug("PNN: PNN is available"); + this.readPNN(); + } else { + if (DEBUG) this.context.debug("PNN: PNN is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("OPL")) { + if (DEBUG) this.context.debug("OPL: OPL is available"); + this.readOPL(); + } else { + if (DEBUG) this.context.debug("OPL: OPL is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("GID1")) { + if (DEBUG) this.context.debug("GID1: GID1 is available"); + this.readGID1(); + } else { + if (DEBUG) this.context.debug("GID1: GID1 is not available"); + } + + if (ICCUtilsHelper.isICCServiceAvailable("CBMI")) { + this.readCBMI(); + } else { + RIL.cellBroadcastConfigs.CBMI = null; + } + if (ICCUtilsHelper.isICCServiceAvailable("DATA_DOWNLOAD_SMS_CB")) { + this.readCBMID(); + } else { + RIL.cellBroadcastConfigs.CBMID = null; + } + if (ICCUtilsHelper.isICCServiceAvailable("CBMIR")) { + this.readCBMIR(); + } else { + RIL.cellBroadcastConfigs.CBMIR = null; + } + RIL._mergeAllCellBroadcastConfigs(); + } + + // ICC_EF_UST has the same value with ICC_EF_SST. + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_SST, + callback: callback.bind(this) + }); + }, + + /** + * Read (U)SIM MBDN. (Mailbox Dialling Number) + * + * @see TS 131.102, clause 4.2.60 + */ + readMBDN: function() { + function callback(options) { + let RIL = this.context.RIL; + let contact = + this.context.ICCPDUHelper.readAlphaIdDiallingNumber(options.recordSize); + if ((!contact || + ((!contact.alphaId || contact.alphaId == "") && + (!contact.number || contact.number == ""))) && + this.context.ICCUtilsHelper.isCphsServiceAvailable("MBN")) { + // read CPHS_MBN in advance if MBDN is invalid or empty. + this.readCphsMBN(); + return; + } + + if (!contact || + (RIL.iccInfoPrivate.mbdn !== undefined && + RIL.iccInfoPrivate.mbdn === contact.number)) { + return; + } + RIL.iccInfoPrivate.mbdn = contact.number; + if (DEBUG) { + this.context.debug("MBDN, alphaId=" + contact.alphaId + + " number=" + contact.number); + } + contact.rilMessageType = "iccmbdn"; + RIL.sendChromeMessage(contact); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_MBDN, + callback: callback.bind(this) + }); + }, + + /** + * Read ICC MWIS. (Message Waiting Indication Status) + * + * @see TS 31.102, clause 4.2.63 for USIM and TS 51.011, clause 10.3.45 for SIM. + */ + readMWIS: function() { + function callback(options) { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let mwis = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + if (!mwis) { + return; + } + RIL.iccInfoPrivate.mwis = mwis; //Keep raw MWIS for updateMWIS() + + let mwi = {}; + // b8 b7 B6 b5 b4 b3 b2 b1 4.2.63, TS 31.102 version 11.6.0 + // | | | | | | | |__ Voicemail + // | | | | | | |_____ Fax + // | | | | | |________ Electronic Mail + // | | | | |___________ Other + // | | | |______________ Videomail + // |__|__|_________________ RFU + mwi.active = ((mwis[0] & 0x01) != 0); + + if (mwi.active) { + // In TS 23.040 msgCount is in the range from 0 to 255. + // The value 255 shall be taken to mean 255 or greater. + // + // However, There is no definition about 0 when MWI is active. + // + // Normally, when mwi is active, the msgCount must be larger than 0. + // Refer to other reference phone, + // 0 is usually treated as UNKNOWN for storing 2nd level MWI status (DCS). + mwi.msgCount = (mwis[1] === 0) ? GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN + : mwis[1]; + } else { + mwi.msgCount = 0; + } + + RIL.sendChromeMessage({ rilMessageType: "iccmwis", + mwi: mwi }); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_MWIS, + recordNumber: 1, // Get 1st Subscriber Profile. + callback: callback.bind(this) + }); + }, + + /** + * Update ICC MWIS. (Message Waiting Indication Status) + * + * @see TS 31.102, clause 4.2.63 for USIM and TS 51.011, clause 10.3.45 for SIM. + */ + updateMWIS: function(mwi) { + let RIL = this.context.RIL; + if (!RIL.iccInfoPrivate.mwis) { + return; + } + + function dataWriter(recordSize) { + let mwis = RIL.iccInfoPrivate.mwis; + + let msgCount = + (mwi.msgCount === GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN) ? 0 : mwi.msgCount; + + [mwis[0], mwis[1]] = (mwi.active) ? [(mwis[0] | 0x01), msgCount] + : [(mwis[0] & 0xFE), 0]; + + let strLen = recordSize * 2; + let Buf = this.context.Buf; + Buf.writeInt32(strLen); + + let GsmPDUHelper = this.context.GsmPDUHelper; + for (let i = 0; i < mwis.length; i++) { + GsmPDUHelper.writeHexOctet(mwis[i]); + } + + Buf.writeStringDelimiter(strLen); + } + + this.context.ICCIOHelper.updateLinearFixedEF({ + fileId: ICC_EF_MWIS, + recordNumber: 1, // Update 1st Subscriber Profile. + dataWriter: dataWriter.bind(this) + }); + }, + + /** + * Read the SPDI (Service Provider Display Information) from the (U)SIM. + * + * See TS 131.102 section 4.2.66 for USIM and TS 51.011 section 10.3.50 + * for SIM. + */ + readSPDI: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + let readLen = 0; + let endLoop = false; + + let RIL = this.context.RIL; + RIL.iccInfoPrivate.SPDI = null; + + let GsmPDUHelper = this.context.GsmPDUHelper; + while ((readLen < octetLen) && !endLoop) { + let tlvTag = GsmPDUHelper.readHexOctet(); + let tlvLen = GsmPDUHelper.readHexOctet(); + readLen += 2; // For tag and length fields. + switch (tlvTag) { + case SPDI_TAG_SPDI: + // The value part itself is a TLV. + continue; + case SPDI_TAG_PLMN_LIST: + // This PLMN list is what we want. + RIL.iccInfoPrivate.SPDI = this.readPLMNEntries(tlvLen / 3); + readLen += tlvLen; + endLoop = true; + break; + default: + // We don't care about its content if its tag is not SPDI nor + // PLMN_LIST. + endLoop = true; + break; + } + } + + // Consume unread octets. + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + + if (DEBUG) { + this.context.debug("SPDI: " + JSON.stringify(RIL.iccInfoPrivate.SPDI)); + } + let ICCUtilsHelper = this.context.ICCUtilsHelper; + if (ICCUtilsHelper.updateDisplayCondition()) { + ICCUtilsHelper.handleICCInfoChange(); + } + } + + // PLMN List is Servive 51 in USIM, EF_SPDI + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_SPDI, + callback: callback.bind(this) + }); + }, + + _readCbmiHelper: function(which) { + let RIL = this.context.RIL; + + function callback() { + let Buf = this.context.Buf; + let strLength = Buf.readInt32(); + + // Each Message Identifier takes two octets and each octet is encoded + // into two chars. + let numIds = strLength / 4, list = null; + if (numIds) { + list = []; + let GsmPDUHelper = this.context.GsmPDUHelper; + for (let i = 0, id; i < numIds; i++) { + id = GsmPDUHelper.readHexOctet() << 8 | GsmPDUHelper.readHexOctet(); + // `Unused entries shall be set to 'FF FF'.` + if (id != 0xFFFF) { + list.push(id); + list.push(id + 1); + } + } + } + if (DEBUG) { + this.context.debug(which + ": " + JSON.stringify(list)); + } + + Buf.readStringDelimiter(strLength); + + RIL.cellBroadcastConfigs[which] = list; + RIL._mergeAllCellBroadcastConfigs(); + } + + function onerror() { + RIL.cellBroadcastConfigs[which] = null; + RIL._mergeAllCellBroadcastConfigs(); + } + + let fileId = GLOBAL["ICC_EF_" + which]; + this.context.ICCIOHelper.loadTransparentEF({ + fileId: fileId, + callback: callback.bind(this), + onerror: onerror.bind(this) + }); + }, + + /** + * Read EFcbmi (Cell Broadcast Message Identifier selection) + * + * @see 3GPP TS 31.102 v110.02.0 section 4.2.14 EFcbmi + * @see 3GPP TS 51.011 v5.0.0 section 10.3.13 EFcbmi + */ + readCBMI: function() { + this._readCbmiHelper("CBMI"); + }, + + /** + * Read EFcbmid (Cell Broadcast Message Identifier for Data Download) + * + * @see 3GPP TS 31.102 v110.02.0 section 4.2.20 EFcbmid + * @see 3GPP TS 51.011 v5.0.0 section 10.3.26 EFcbmid + */ + readCBMID: function() { + this._readCbmiHelper("CBMID"); + }, + + /** + * Read EFcbmir (Cell Broadcast Message Identifier Range selection) + * + * @see 3GPP TS 31.102 v110.02.0 section 4.2.22 EFcbmir + * @see 3GPP TS 51.011 v5.0.0 section 10.3.28 EFcbmir + */ + readCBMIR: function() { + let RIL = this.context.RIL; + + function callback() { + let Buf = this.context.Buf; + let strLength = Buf.readInt32(); + + // Each Message Identifier range takes four octets and each octet is + // encoded into two chars. + let numIds = strLength / 8, list = null; + if (numIds) { + list = []; + let GsmPDUHelper = this.context.GsmPDUHelper; + for (let i = 0, from, to; i < numIds; i++) { + // `Bytes one and two of each range identifier equal the lower value + // of a cell broadcast range, bytes three and four equal the upper + // value of a cell broadcast range.` + from = GsmPDUHelper.readHexOctet() << 8 | GsmPDUHelper.readHexOctet(); + to = GsmPDUHelper.readHexOctet() << 8 | GsmPDUHelper.readHexOctet(); + // `Unused entries shall be set to 'FF FF'.` + if ((from != 0xFFFF) && (to != 0xFFFF)) { + list.push(from); + list.push(to + 1); + } + } + } + if (DEBUG) { + this.context.debug("CBMIR: " + JSON.stringify(list)); + } + + Buf.readStringDelimiter(strLength); + + RIL.cellBroadcastConfigs.CBMIR = list; + RIL._mergeAllCellBroadcastConfigs(); + } + + function onerror() { + RIL.cellBroadcastConfigs.CBMIR = null; + RIL._mergeAllCellBroadcastConfigs(); + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CBMIR, + callback: callback.bind(this), + onerror: onerror.bind(this) + }); + }, + + /** + * Read OPL (Operator PLMN List) from (U)SIM. + * + * See 3GPP TS 31.102 Sec. 4.2.59 for USIM + * 3GPP TS 51.011 Sec. 10.3.42 for SIM. + */ + readOPL: function() { + let ICCIOHelper = this.context.ICCIOHelper; + let opl = []; + function callback(options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let strLen = Buf.readInt32(); + // The first 7 bytes are LAI (for UMTS) and the format of LAI is defined + // in 3GPP TS 23.003, Sec 4.1 + // +-------------+---------+ + // | Octet 1 - 3 | MCC/MNC | + // +-------------+---------+ + // | Octet 4 - 7 | LAC | + // +-------------+---------+ + let mccMnc = [GsmPDUHelper.readHexOctet(), + GsmPDUHelper.readHexOctet(), + GsmPDUHelper.readHexOctet()]; + if (mccMnc[0] != 0xFF || mccMnc[1] != 0xFF || mccMnc[2] != 0xFF) { + let oplElement = {}; + let semiOctets = []; + for (let i = 0; i < mccMnc.length; i++) { + semiOctets.push((mccMnc[i] & 0xf0) >> 4); + semiOctets.push(mccMnc[i] & 0x0f); + } + let reformat = [semiOctets[1], semiOctets[0], semiOctets[3], + semiOctets[5], semiOctets[4], semiOctets[2]]; + let buf = ""; + for (let i = 0; i < reformat.length; i++) { + if (reformat[i] != 0xF) { + buf += GsmPDUHelper.semiOctetToExtendedBcdChar(reformat[i]); + } + if (i === 2) { + // 0-2: MCC + oplElement.mcc = buf; + buf = ""; + } else if (i === 5) { + // 3-5: MNC + oplElement.mnc = buf; + } + } + // LAC/TAC + oplElement.lacTacStart = + (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + oplElement.lacTacEnd = + (GsmPDUHelper.readHexOctet() << 8) | GsmPDUHelper.readHexOctet(); + // PLMN Network Name Record Identifier + oplElement.pnnRecordId = GsmPDUHelper.readHexOctet(); + if (DEBUG) { + this.context.debug("OPL: [" + (opl.length + 1) + "]: " + + JSON.stringify(oplElement)); + } + opl.push(oplElement); + } else { + Buf.seekIncoming(5 * Buf.PDU_HEX_OCTET_SIZE); + } + Buf.readStringDelimiter(strLen); + + let RIL = this.context.RIL; + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + } else { + RIL.iccInfoPrivate.OPL = opl; + RIL.overrideICCNetworkName(); + } + } + + ICCIOHelper.loadLinearFixedEF({fileId: ICC_EF_OPL, + callback: callback.bind(this)}); + }, + + /** + * Read PNN (PLMN Network Name) from (U)SIM. + * + * See 3GPP TS 31.102 Sec. 4.2.58 for USIM + * 3GPP TS 51.011 Sec. 10.3.41 for SIM. + */ + readPNN: function() { + let ICCIOHelper = this.context.ICCIOHelper; + function callback(options) { + let pnnElement; + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + let readLen = 0; + + let GsmPDUHelper = this.context.GsmPDUHelper; + while (readLen < octetLen) { + let tlvTag = GsmPDUHelper.readHexOctet(); + + if (tlvTag == 0xFF) { + // Unused byte + readLen++; + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + break; + } + + // Needs this check to avoid initializing twice. + pnnElement = pnnElement || {}; + + let tlvLen = GsmPDUHelper.readHexOctet(); + + switch (tlvTag) { + case PNN_IEI_FULL_NETWORK_NAME: + pnnElement.fullName = GsmPDUHelper.readNetworkName(tlvLen); + break; + case PNN_IEI_SHORT_NETWORK_NAME: + pnnElement.shortName = GsmPDUHelper.readNetworkName(tlvLen); + break; + default: + Buf.seekIncoming(tlvLen * Buf.PDU_HEX_OCTET_SIZE); + break; + } + + readLen += (tlvLen + 2); // +2 for tlvTag and tlvLen + } + Buf.readStringDelimiter(strLen); + + pnn.push(pnnElement); + + let RIL = this.context.RIL; + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + } else { + if (DEBUG) { + for (let i = 0; i < pnn.length; i++) { + this.context.debug("PNN: [" + i + "]: " + JSON.stringify(pnn[i])); + } + } + RIL.iccInfoPrivate.PNN = pnn; + RIL.overrideICCNetworkName(); + } + } + + let pnn = []; + ICCIOHelper.loadLinearFixedEF({fileId: ICC_EF_PNN, + callback: callback.bind(this)}); + }, + + /** + * Read the list of PLMN (Public Land Mobile Network) entries + * We cannot directly rely on readSwappedNibbleBcdToString(), + * since it will no correctly handle some corner-cases that are + * not a problem in our case (0xFF 0xFF 0xFF). + * + * @param length The number of PLMN records. + * @return An array of string corresponding to the PLMNs. + */ + readPLMNEntries: function(length) { + let plmnList = []; + // Each PLMN entry has 3 bytes. + if (DEBUG) { + this.context.debug("PLMN entries length = " + length); + } + let GsmPDUHelper = this.context.GsmPDUHelper; + let index = 0; + while (index < length) { + // Unused entries will be 0xFFFFFF, according to EF_SPDI + // specs (TS 131 102, section 4.2.66) + try { + let plmn = [GsmPDUHelper.readHexOctet(), + GsmPDUHelper.readHexOctet(), + GsmPDUHelper.readHexOctet()]; + if (DEBUG) { + this.context.debug("Reading PLMN entry: [" + index + "]: '" + plmn + "'"); + } + if (plmn[0] != 0xFF && + plmn[1] != 0xFF && + plmn[2] != 0xFF) { + let semiOctets = []; + for (let idx = 0; idx < plmn.length; idx++) { + semiOctets.push((plmn[idx] & 0xF0) >> 4); + semiOctets.push(plmn[idx] & 0x0F); + } + + // According to TS 24.301, 9.9.3.12, the semi octets is arranged + // in format: + // Byte 1: MCC[2] | MCC[1] + // Byte 2: MNC[3] | MCC[3] + // Byte 3: MNC[2] | MNC[1] + // Therefore, we need to rearrange them. + let reformat = [semiOctets[1], semiOctets[0], semiOctets[3], + semiOctets[5], semiOctets[4], semiOctets[2]]; + let buf = ""; + let plmnEntry = {}; + for (let i = 0; i < reformat.length; i++) { + if (reformat[i] != 0xF) { + buf += GsmPDUHelper.semiOctetToExtendedBcdChar(reformat[i]); + } + if (i === 2) { + // 0-2: MCC + plmnEntry.mcc = buf; + buf = ""; + } else if (i === 5) { + // 3-5: MNC + plmnEntry.mnc = buf; + } + } + if (DEBUG) { + this.context.debug("PLMN = " + plmnEntry.mcc + ", " + plmnEntry.mnc); + } + plmnList.push(plmnEntry); + } + } catch (e) { + if (DEBUG) { + this.context.debug("PLMN entry " + index + " is invalid."); + } + break; + } + index ++; + } + return plmnList; + }, + + /** + * Read the SMS from the ICC. + * + * @param recordNumber The number of the record shall be loaded. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readSMS: function(recordNumber, onsuccess, onerror) { + function callback(options) { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + + // TS 51.011, 10.5.3 EF_SMS + // b3 b2 b1 + // 0 0 1 message received by MS from network; message read + // 0 1 1 message received by MS from network; message to be read + // 1 1 1 MS originating message; message to be sent + // 1 0 1 MS originating message; message sent to the network: + let GsmPDUHelper = this.context.GsmPDUHelper; + let status = GsmPDUHelper.readHexOctet(); + + let message = GsmPDUHelper.readMessage(); + message.simStatus = status; + + // Consumes the remaining buffer + Buf.seekIncoming(Buf.getReadAvailable() - Buf.PDU_HEX_OCTET_SIZE); + + Buf.readStringDelimiter(strLen); + + if (message) { + onsuccess(message); + } else { + onerror("Failed to decode SMS on SIM #" + recordNumber); + } + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_SMS, + recordNumber: recordNumber, + callback: callback.bind(this), + onerror: onerror + }); + }, + + readGID1: function() { + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + RIL.iccInfoPrivate.gid1 = Buf.readString(); + if (DEBUG) { + this.context.debug("GID1: " + RIL.iccInfoPrivate.gid1); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_GID1, + callback: callback.bind(this) + }); + }, + + /** + * Read CPHS Phase & Service Table from CPHS Info. + * + * @See B.3.1.1 CPHS Information in CPHS Phase 2. + * + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readCphsInfo: function(onsuccess, onerror) { + function callback() { + try { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + let octetLen = strLen / 2; + let cphsInfo = this.context.GsmPDUHelper.readHexOctetArray(octetLen); + Buf.readStringDelimiter(strLen); + if (DEBUG) { + let str = ""; + for (let i = 0; i < cphsInfo.length; i++) { + str += cphsInfo[i] + ", "; + } + this.context.debug("CPHS INFO: " + str); + } + + /** + * CPHS INFORMATION + * + * Byte 1: CPHS Phase + * 01 phase 1 + * 02 phase 2 + * etc. + * + * Byte 2: CPHS Service Table + * +----+----+----+----+----+----+----+----+ + * | b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | + * +----+----+----+----+----+----+----+----+ + * | ONSF | MBN | SST | CSP | + * | Phase 2 | ALL | Phase 1 | All | + * +----+----+----+----+----+----+----+----+ + * + * Byte 3: CPHS Service Table continued + * +----+----+----+----+----+----+----+----+ + * | b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | + * +----+----+----+----+----+----+----+----+ + * | RFU | RFU | RFU | INFO_NUM| + * | | | | Phase 2 | + * +----+----+----+----+----+----+----+----+ + */ + let cphsPhase = cphsInfo[0]; + if (cphsPhase == 1) { + // Clear 'Phase 2 only' services. + cphsInfo[1] &= 0x3F; + // We don't know whether Byte 3 is available in CPHS phase 1 or not. + // Add boundary check before accessing it. + if (cphsInfo.length > 2) { + cphsInfo[2] = 0x00; + } + } else if (cphsPhase == 2) { + // Clear 'Phase 1 only' services. + cphsInfo[1] &= 0xF3; + } else { + throw new Error("Unknown CPHS phase: " + cphsPhase); + } + + RIL.iccInfoPrivate.cphsSt = cphsInfo.subarray(1); + onsuccess(); + } catch(e) { + onerror(e.toString()); + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CPHS_INFO, + callback: callback.bind(this), + onerror: onerror + }); + }, + + /** + * Read CPHS MBN. (Mailbox Numbers) + * + * @See B.4.2.2 Voice Message Retrieval and Indicator Clearing + */ + readCphsMBN: function() { + function callback(options) { + let RIL = this.context.RIL; + let contact = + this.context.ICCPDUHelper.readAlphaIdDiallingNumber(options.recordSize); + if (!contact || + (RIL.iccInfoPrivate.mbdn !== undefined && + RIL.iccInfoPrivate.mbdn === contact.number)) { + return; + } + RIL.iccInfoPrivate.mbdn = contact.number; + if (DEBUG) { + this.context.debug("CPHS_MDN, alphaId=" + contact.alphaId + + " number=" + contact.number); + } + contact.rilMessageType = "iccmbdn"; + RIL.sendChromeMessage(contact); + } + + this.context.ICCIOHelper.loadLinearFixedEF({ + fileId: ICC_EF_CPHS_MBN, + callback: callback.bind(this) + }); + } +}; + +function RuimRecordHelperObject(aContext) { + this.context = aContext; +} +RuimRecordHelperObject.prototype = { + context: null, + + fetchRuimRecords: function() { + this.getIMSI_M(); + this.readCST(); + this.readCDMAHome(); + this.context.RIL.getCdmaSubscription(); + }, + + /** + * Get IMSI_M from CSIM/RUIM. + * See 3GPP2 C.S0065 Sec. 5.2.2 + */ + getIMSI_M: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let encodedImsi = this.context.GsmPDUHelper.readHexOctetArray(strLen / 2); + Buf.readStringDelimiter(strLen); + + if ((encodedImsi[CSIM_IMSI_M_PROGRAMMED_BYTE] & 0x80)) { // IMSI_M programmed + let RIL = this.context.RIL; + RIL.iccInfoPrivate.imsi = this.decodeIMSI(encodedImsi); + RIL.sendChromeMessage({rilMessageType: "iccimsi", + imsi: RIL.iccInfoPrivate.imsi}); + + let ICCUtilsHelper = this.context.ICCUtilsHelper; + let mccMnc = ICCUtilsHelper.parseMccMncFromImsi(RIL.iccInfoPrivate.imsi); + if (mccMnc) { + RIL.iccInfo.mcc = mccMnc.mcc; + RIL.iccInfo.mnc = mccMnc.mnc; + ICCUtilsHelper.handleICCInfoChange(); + } + } + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CSIM_IMSI_M, + callback: callback.bind(this) + }); + }, + + /** + * Decode IMSI from IMSI_M + * See 3GPP2 C.S0005 Sec. 2.3.1 + * +---+---------+------------+---+--------+---------+---+---------+--------+ + * |RFU| MCC | programmed |RFU| MNC | MIN1 |RFU| MIN2 | CLASS | + * +---+---------+------------+---+--------+---------+---+---------+--------+ + * | 6 | 10 bits | 8 bits | 1 | 7 bits | 24 bits | 6 | 10 bits | 8 bits | + * +---+---------+------------+---+--------+---------+---+---------+--------+ + */ + decodeIMSI: function(encodedImsi) { + // MCC: 10 bits, 3 digits + let encodedMCC = ((encodedImsi[CSIM_IMSI_M_MCC_BYTE + 1] & 0x03) << 8) + + (encodedImsi[CSIM_IMSI_M_MCC_BYTE] & 0xff); + let mcc = this.decodeIMSIValue(encodedMCC, 3); + + // MNC: 7 bits, 2 digits + let encodedMNC = encodedImsi[CSIM_IMSI_M_MNC_BYTE] & 0x7f; + let mnc = this.decodeIMSIValue(encodedMNC, 2); + + // MIN2: 10 bits, 3 digits + let encodedMIN2 = ((encodedImsi[CSIM_IMSI_M_MIN2_BYTE + 1] & 0x03) << 8) + + (encodedImsi[CSIM_IMSI_M_MIN2_BYTE] & 0xff); + let min2 = this.decodeIMSIValue(encodedMIN2, 3); + + // MIN1: 10+4+10 bits, 3+1+3 digits + let encodedMIN1First3 = ((encodedImsi[CSIM_IMSI_M_MIN1_BYTE + 2] & 0xff) << 2) + + ((encodedImsi[CSIM_IMSI_M_MIN1_BYTE + 1] & 0xc0) >> 6); + let min1First3 = this.decodeIMSIValue(encodedMIN1First3, 3); + + let encodedFourthDigit = (encodedImsi[CSIM_IMSI_M_MIN1_BYTE + 1] & 0x3c) >> 2; + if (encodedFourthDigit > 9) { + encodedFourthDigit = 0; + } + let fourthDigit = encodedFourthDigit.toString(); + + let encodedMIN1Last3 = ((encodedImsi[CSIM_IMSI_M_MIN1_BYTE + 1] & 0x03) << 8) + + (encodedImsi[CSIM_IMSI_M_MIN1_BYTE] & 0xff); + let min1Last3 = this.decodeIMSIValue(encodedMIN1Last3, 3); + + return mcc + mnc + min2 + min1First3 + fourthDigit + min1Last3; + }, + + /** + * Decode IMSI Helper function + * See 3GPP2 C.S0005 section 2.3.1.1 + */ + decodeIMSIValue: function(encoded, length) { + let offset = length === 3 ? 111 : 11; + let value = encoded + offset; + + for (let base = 10, temp = value, i = 0; i < length; i++) { + if (temp % 10 === 0) { + value -= base; + } + temp = Math.floor(value / base); + base = base * 10; + } + + let s = value.toString(); + while (s.length < length) { + s = "0" + s; + } + + return s; + }, + + /** + * Read CDMAHOME for CSIM. + * See 3GPP2 C.S0023 Sec. 3.4.8. + */ + readCDMAHome: function() { + let ICCIOHelper = this.context.ICCIOHelper; + + function callback(options) { + let Buf = this.context.Buf; + let GsmPDUHelper = this.context.GsmPDUHelper; + + let strLen = Buf.readInt32(); + let tempOctet = GsmPDUHelper.readHexOctet(); + cdmaHomeSystemId.push(((GsmPDUHelper.readHexOctet() & 0x7f) << 8) | tempOctet); + tempOctet = GsmPDUHelper.readHexOctet(); + cdmaHomeNetworkId.push(((GsmPDUHelper.readHexOctet() & 0xff) << 8) | tempOctet); + + // Consuming the last octet: band class. + Buf.seekIncoming(Buf.PDU_HEX_OCTET_SIZE); + + Buf.readStringDelimiter(strLen); + if (options.p1 < options.totalRecords) { + ICCIOHelper.loadNextRecord(options); + } else { + if (DEBUG) { + this.context.debug("CDMAHome system id: " + + JSON.stringify(cdmaHomeSystemId)); + this.context.debug("CDMAHome network id: " + + JSON.stringify(cdmaHomeNetworkId)); + } + this.context.RIL.cdmaHome = { + systemId: cdmaHomeSystemId, + networkId: cdmaHomeNetworkId + }; + } + } + + let cdmaHomeSystemId = [], cdmaHomeNetworkId = []; + ICCIOHelper.loadLinearFixedEF({fileId: ICC_EF_CSIM_CDMAHOME, + callback: callback.bind(this)}); + }, + + /** + * Read CDMA Service Table. + * See 3GPP2 C.S0023 Sec. 3.4.18 + */ + readCST: function() { + function callback() { + let Buf = this.context.Buf; + let RIL = this.context.RIL; + + let strLen = Buf.readInt32(); + // Each octet is encoded into two chars. + RIL.iccInfoPrivate.cst = + this.context.GsmPDUHelper.readHexOctetArray(strLen / 2); + Buf.readStringDelimiter(strLen); + + if (DEBUG) { + let str = ""; + for (let i = 0; i < RIL.iccInfoPrivate.cst.length; i++) { + str += RIL.iccInfoPrivate.cst[i] + ", "; + } + this.context.debug("CST: " + str); + } + + if (this.context.ICCUtilsHelper.isICCServiceAvailable("SPN")) { + if (DEBUG) this.context.debug("SPN: SPN is available"); + this.readSPN(); + } + } + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CSIM_CST, + callback: callback.bind(this) + }); + }, + + readSPN: function() { + function callback() { + let Buf = this.context.Buf; + let strLen = Buf.readInt32(); + let octetLen = strLen / 2; + + let GsmPDUHelper = this.context.GsmPDUHelper; + let displayCondition = GsmPDUHelper.readHexOctet(); + let codingScheme = GsmPDUHelper.readHexOctet(); + // Skip one octet: language indicator. + Buf.seekIncoming(Buf.PDU_HEX_OCTET_SIZE); + let readLen = 3; + + // SPN String ends up with 0xff. + let userDataBuffer = []; + + while (readLen < octetLen) { + let octet = GsmPDUHelper.readHexOctet(); + readLen++; + if (octet == 0xff) { + break; + } + userDataBuffer.push(octet); + } + + this.context.BitBufferHelper.startRead(userDataBuffer); + + let CdmaPDUHelper = this.context.CdmaPDUHelper; + let msgLen; + switch (CdmaPDUHelper.getCdmaMsgEncoding(codingScheme)) { + case PDU_DCS_MSG_CODING_7BITS_ALPHABET: + msgLen = Math.floor(userDataBuffer.length * 8 / 7); + break; + case PDU_DCS_MSG_CODING_8BITS_ALPHABET: + msgLen = userDataBuffer.length; + break; + case PDU_DCS_MSG_CODING_16BITS_ALPHABET: + msgLen = Math.floor(userDataBuffer.length / 2); + break; + } + + let RIL = this.context.RIL; + RIL.iccInfo.spn = CdmaPDUHelper.decodeCdmaPDUMsg(codingScheme, null, msgLen); + if (DEBUG) { + this.context.debug("CDMA SPN: " + RIL.iccInfo.spn + + ", Display condition: " + displayCondition); + } + RIL.iccInfoPrivate.spnDisplayCondition = displayCondition; + Buf.seekIncoming((octetLen - readLen) * Buf.PDU_HEX_OCTET_SIZE); + Buf.readStringDelimiter(strLen); + } + + this.context.ICCIOHelper.loadTransparentEF({ + fileId: ICC_EF_CSIM_SPN, + callback: callback.bind(this) + }); + } +}; + +/** + * Helper functions for ICC utilities. + */ +function ICCUtilsHelperObject(aContext) { + this.context = aContext; +} +ICCUtilsHelperObject.prototype = { + context: null, + + /** + * Get network names by using EF_OPL and EF_PNN + * + * @See 3GPP TS 31.102 sec. 4.2.58 and sec. 4.2.59 for USIM, + * 3GPP TS 51.011 sec. 10.3.41 and sec. 10.3.42 for SIM. + * + * @param mcc The mobile country code of the network. + * @param mnc The mobile network code of the network. + * @param lac The location area code of the network. + */ + getNetworkNameFromICC: function(mcc, mnc, lac) { + let RIL = this.context.RIL; + let iccInfoPriv = RIL.iccInfoPrivate; + let iccInfo = RIL.iccInfo; + let pnnEntry; + + if (!mcc || !mnc || lac == null || lac < 0) { + return null; + } + + // We won't get network name if there is no PNN file. + if (!iccInfoPriv.PNN) { + return null; + } + + if (!this.isICCServiceAvailable("OPL")) { + // When OPL is not present: + // According to 3GPP TS 31.102 Sec. 4.2.58 and 3GPP TS 51.011 Sec. 10.3.41, + // If EF_OPL is not present, the first record in this EF is used for the + // default network name when registered to the HPLMN. + // If we haven't get pnnEntry assigned, we should try to assign default + // value to it. + if (mcc == iccInfo.mcc && mnc == iccInfo.mnc) { + pnnEntry = iccInfoPriv.PNN[0]; + } + } else { + let GsmPDUHelper = this.context.GsmPDUHelper; + let wildChar = GsmPDUHelper.extendedBcdChars.charAt(0x0d); + // According to 3GPP TS 31.102 Sec. 4.2.59 and 3GPP TS 51.011 Sec. 10.3.42, + // the ME shall use this EF_OPL in association with the EF_PNN in place + // of any network name stored within the ME's internal list and any network + // name received when registered to the PLMN. + let length = iccInfoPriv.OPL ? iccInfoPriv.OPL.length : 0; + for (let i = 0; i < length; i++) { + let unmatch = false; + let opl = iccInfoPriv.OPL[i]; + // Try to match the MCC/MNC. Besides, A BCD value of 'D' in any of the + // MCC and/or MNC digits shall be used to indicate a "wild" value for + // that corresponding MCC/MNC digit. + if (opl.mcc.indexOf(wildChar) !== -1) { + for (let j = 0; j < opl.mcc.length; j++) { + if (opl.mcc[j] !== wildChar && opl.mcc[j] !== mcc[j]) { + unmatch = true; + break; + } + } + if (unmatch) { + continue; + } + } else { + if (mcc !== opl.mcc) { + continue; + } + } + + if (mnc.length !== opl.mnc.length) { + continue; + } + + if (opl.mnc.indexOf(wildChar) !== -1) { + for (let j = 0; j < opl.mnc.length; j++) { + if (opl.mnc[j] !== wildChar && opl.mnc[j] !== mnc[j]) { + unmatch = true; + break; + } + } + if (unmatch) { + continue; + } + } else { + if (mnc !== opl.mnc) { + continue; + } + } + + // Try to match the location area code. If current local area code is + // covered by lac range that specified in the OPL entry, use the PNN + // that specified in the OPL entry. + if ((opl.lacTacStart === 0x0 && opl.lacTacEnd == 0xFFFE) || + (opl.lacTacStart <= lac && opl.lacTacEnd >= lac)) { + if (opl.pnnRecordId === 0) { + // See 3GPP TS 31.102 Sec. 4.2.59 and 3GPP TS 51.011 Sec. 10.3.42, + // A value of '00' indicates that the name is to be taken from other + // sources. + return null; + } + pnnEntry = iccInfoPriv.PNN[opl.pnnRecordId - 1]; + break; + } + } + } + + if (!pnnEntry) { + return null; + } + + // Return a new object to avoid global variable, PNN, be modified by accident. + return { fullName: pnnEntry.fullName || "", + shortName: pnnEntry.shortName || "" }; + }, + + /** + * This will compute the spnDisplay field of the network. + * See TS 22.101 Annex A and TS 51.011 10.3.11 for details. + * + * @return True if some of iccInfo is changed in by this function. + */ + updateDisplayCondition: function() { + let RIL = this.context.RIL; + + // If EFspn isn't existed in SIM or it haven't been read yet, we should + // just set isDisplayNetworkNameRequired = true and + // isDisplaySpnRequired = false + let iccInfo = RIL.iccInfo; + let iccInfoPriv = RIL.iccInfoPrivate; + let displayCondition = iccInfoPriv.spnDisplayCondition; + let origIsDisplayNetworkNameRequired = iccInfo.isDisplayNetworkNameRequired; + let origIsDisplaySPNRequired = iccInfo.isDisplaySpnRequired; + + if (displayCondition === undefined) { + iccInfo.isDisplayNetworkNameRequired = true; + iccInfo.isDisplaySpnRequired = false; + } else if (RIL._isCdma) { + // CDMA family display rule. + let cdmaHome = RIL.cdmaHome; + let cell = RIL.voiceRegistrationState.cell; + let sid = cell && cell.cdmaSystemId; + let nid = cell && cell.cdmaNetworkId; + + iccInfo.isDisplayNetworkNameRequired = false; + + // If display condition is 0x0, we don't even need to check network id + // or system id. + if (displayCondition === 0x0) { + iccInfo.isDisplaySpnRequired = false; + } else { + // CDMA SPN Display condition dosen't specify whenever network name is + // reqired. + if (!cdmaHome || + !cdmaHome.systemId || + cdmaHome.systemId.length === 0 || + cdmaHome.systemId.length != cdmaHome.networkId.length || + !sid || !nid) { + // CDMA Home haven't been ready, or we haven't got the system id and + // network id of the network we register to, assuming we are in home + // network. + iccInfo.isDisplaySpnRequired = true; + } else { + // Determine if we are registered in the home service area. + // System ID and Network ID are described in 3GPP2 C.S0005 Sec. 2.6.5.2. + let inHomeArea = false; + for (let i = 0; i < cdmaHome.systemId.length; i++) { + let homeSid = cdmaHome.systemId[i], + homeNid = cdmaHome.networkId[i]; + if (homeSid === 0 || homeNid === 0 // Reserved system id/network id + || homeSid != sid) { + continue; + } + // According to 3GPP2 C.S0005 Sec. 2.6.5.2, NID number 65535 means + // all networks in the system should be considered as home. + if (homeNid == 65535 || homeNid == nid) { + inHomeArea = true; + break; + } + } + iccInfo.isDisplaySpnRequired = inHomeArea; + } + } + } else { + // GSM family display rule. + let operatorMnc = RIL.operator ? RIL.operator.mnc : -1; + let operatorMcc = RIL.operator ? RIL.operator.mcc : -1; + + // First detect if we are on HPLMN or one of the PLMN + // specified by the SIM card. + let isOnMatchingPlmn = false; + + // If the current network is the one defined as mcc/mnc + // in SIM card, it's okay. + if (iccInfo.mcc == operatorMcc && iccInfo.mnc == operatorMnc) { + isOnMatchingPlmn = true; + } + + // Test to see if operator's mcc/mnc match mcc/mnc of PLMN. + if (!isOnMatchingPlmn && iccInfoPriv.SPDI) { + let iccSpdi = iccInfoPriv.SPDI; // PLMN list + for (let plmn in iccSpdi) { + let plmnMcc = iccSpdi[plmn].mcc; + let plmnMnc = iccSpdi[plmn].mnc; + isOnMatchingPlmn = (plmnMcc == operatorMcc) && (plmnMnc == operatorMnc); + if (isOnMatchingPlmn) { + break; + } + } + } + + // See 3GPP TS 22.101 A.4 Service Provider Name indication, and TS 31.102 + // clause 4.2.12 EF_SPN for detail. + if (isOnMatchingPlmn) { + // The first bit of display condition tells us if we should display + // registered PLMN. + if (DEBUG) { + this.context.debug("PLMN is HPLMN or PLMN " + "is in PLMN list"); + } + + // TS 31.102 Sec. 4.2.66 and TS 51.011 Sec. 10.3.50 + // EF_SPDI contains a list of PLMNs in which the Service Provider Name + // shall be displayed. + iccInfo.isDisplaySpnRequired = true; + iccInfo.isDisplayNetworkNameRequired = (displayCondition & 0x01) !== 0; + } else { + // The second bit of display condition tells us if we should display + // registered PLMN. + if (DEBUG) { + this.context.debug("PLMN isn't HPLMN and PLMN isn't in PLMN list"); + } + + iccInfo.isDisplayNetworkNameRequired = true; + iccInfo.isDisplaySpnRequired = (displayCondition & 0x02) === 0; + } + } + + if (DEBUG) { + this.context.debug("isDisplayNetworkNameRequired = " + + iccInfo.isDisplayNetworkNameRequired); + this.context.debug("isDisplaySpnRequired = " + iccInfo.isDisplaySpnRequired); + } + + return ((origIsDisplayNetworkNameRequired !== iccInfo.isDisplayNetworkNameRequired) || + (origIsDisplaySPNRequired !== iccInfo.isDisplaySpnRequired)); + }, + + decodeSimTlvs: function(tlvsLen) { + let GsmPDUHelper = this.context.GsmPDUHelper; + + let index = 0; + let tlvs = []; + while (index < tlvsLen) { + let simTlv = { + tag : GsmPDUHelper.readHexOctet(), + length : GsmPDUHelper.readHexOctet(), + }; + simTlv.value = GsmPDUHelper.readHexOctetArray(simTlv.length); + tlvs.push(simTlv); + index += simTlv.length + 2; // The length of 'tag' and 'length' field. + } + return tlvs; + }, + + /** + * Parse those TLVs and convert it to an object. + */ + parsePbrTlvs: function(pbrTlvs) { + let pbr = {}; + for (let i = 0; i < pbrTlvs.length; i++) { + let pbrTlv = pbrTlvs[i]; + let anrIndex = 0; + for (let j = 0; j < pbrTlv.value.length; j++) { + let tlv = pbrTlv.value[j]; + let tagName = USIM_TAG_NAME[tlv.tag]; + + // ANR could have multiple files. We save it as anr0, anr1,...etc. + if (tlv.tag == ICC_USIM_EFANR_TAG) { + tagName += anrIndex; + anrIndex++; + } + pbr[tagName] = tlv; + pbr[tagName].fileType = pbrTlv.tag; + pbr[tagName].fileId = (tlv.value[0] << 8) | tlv.value[1]; + pbr[tagName].sfi = tlv.value[2]; + + // For Type 2, the order of files is in the same order in IAP. + if (pbrTlv.tag == ICC_USIM_TYPE2_TAG) { + pbr[tagName].indexInIAP = j; + } + } + } + + return pbr; + }, + + /** + * Update the ICC information to RadioInterfaceLayer. + */ + handleICCInfoChange: function() { + let RIL = this.context.RIL; + RIL.iccInfo.rilMessageType = "iccinfochange"; + RIL.sendChromeMessage(RIL.iccInfo); + }, + + /** + * Get whether specificed (U)SIM service is available. + * + * @param geckoService + * Service name like "ADN", "BDN", etc. + * + * @return true if the service is enabled, false otherwise. + */ + isICCServiceAvailable: function(geckoService) { + let RIL = this.context.RIL; + let serviceTable = RIL._isCdma ? RIL.iccInfoPrivate.cst: + RIL.iccInfoPrivate.sst; + let index, bitmask; + if (RIL.appType == CARD_APPTYPE_SIM || RIL.appType == CARD_APPTYPE_RUIM) { + /** + * Service id is valid in 1..N, and 2 bits are used to code each service. + * + * +----+-- --+----+----+ + * | b8 | ... | b2 | b1 | + * +----+-- --+----+----+ + * + * b1 = 0, service not allocated. + * 1, service allocated. + * b2 = 0, service not activated. + * 1, service activated. + * + * @see 3GPP TS 51.011 10.3.7. + */ + let simService; + if (RIL.appType == CARD_APPTYPE_SIM) { + simService = GECKO_ICC_SERVICES.sim[geckoService]; + } else { + simService = GECKO_ICC_SERVICES.ruim[geckoService]; + } + if (!simService) { + return false; + } + simService -= 1; + index = Math.floor(simService / 4); + bitmask = 2 << ((simService % 4) << 1); + } else if (RIL.appType == CARD_APPTYPE_USIM) { + /** + * Service id is valid in 1..N, and 1 bit is used to code each service. + * + * +----+-- --+----+----+ + * | b8 | ... | b2 | b1 | + * +----+-- --+----+----+ + * + * b1 = 0, service not avaiable. + * 1, service available. + * + * @see 3GPP TS 31.102 4.2.8. + */ + let usimService = GECKO_ICC_SERVICES.usim[geckoService]; + if (!usimService) { + return false; + } + usimService -= 1; + index = Math.floor(usimService / 8); + bitmask = 1 << ((usimService % 8) << 0); + } + + return (serviceTable !== null) && + (index < serviceTable.length) && + ((serviceTable[index] & bitmask) !== 0); + }, + + /** + * Get whether specificed CPHS service is available. + * + * @param geckoService + * Service name like "MDN", etc. + * + * @return true if the service is enabled, false otherwise. + */ + isCphsServiceAvailable: function(geckoService) { + let RIL = this.context.RIL; + let serviceTable = RIL.iccInfoPrivate.cphsSt; + + if (!(serviceTable instanceof Uint8Array)) { + return false; + } + + /** + * Service id is valid in 1..N, and 2 bits are used to code each service. + * + * +----+-- --+----+----+ + * | b8 | ... | b2 | b1 | + * +----+-- --+----+----+ + * + * b1 = 0, service not allocated. + * 1, service allocated. + * b2 = 0, service not activated. + * 1, service activated. + * + * @See B.3.1.1 CPHS Information in CPHS Phase 2. + */ + let cphsService = GECKO_ICC_SERVICES.cphs[geckoService]; + + if (!cphsService) { + return false; + } + cphsService -= 1; + let index = Math.floor(cphsService / 4); + let bitmask = 2 << ((cphsService % 4) << 1); + + return (index < serviceTable.length) && + ((serviceTable[index] & bitmask) !== 0); + }, + + /** + * Check if the string is of GSM default 7-bit coded alphabets with bit 8 + * set to 0. + * + * @param str String to be checked. + */ + isGsm8BitAlphabet: function(str) { + if (!str) { + return false; + } + + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + for (let i = 0; i < str.length; i++) { + let c = str.charAt(i); + let octet = langTable.indexOf(c); + if (octet == -1) { + octet = langShiftTable.indexOf(c); + if (octet == -1) { + return false; + } + } + } + + return true; + }, + + /** + * Parse MCC/MNC from IMSI. If there is no available value for the length of + * mnc, it will use the data in MCC table to parse. + * + * @param imsi + * The imsi of icc. + * @param mncLength [optional] + * The length of mnc. + * Zero indicates we haven't got a valid mnc length. + * + * @return An object contains the parsing result of mcc and mnc. + * Or null if any error occurred. + */ + parseMccMncFromImsi: function(imsi, mncLength) { + if (!imsi) { + return null; + } + + // MCC is the first 3 digits of IMSI. + let mcc = imsi.substr(0,3); + if (!mncLength) { + // Check the MCC/MNC table for MNC length = 3 first for the case we don't + // have the 4th byte data from EF_AD. + if (PLMN_HAVING_3DIGITS_MNC[mcc] && + PLMN_HAVING_3DIGITS_MNC[mcc].indexOf(imsi.substr(3, 3)) !== -1) { + mncLength = 3; + } else { + // Check the MCC table to decide the length of MNC. + let index = MCC_TABLE_FOR_MNC_LENGTH_IS_3.indexOf(mcc); + mncLength = (index !== -1) ? 3 : 2; + } + } + let mnc = imsi.substr(3, mncLength); + if (DEBUG) { + this.context.debug("IMSI: " + imsi + " MCC: " + mcc + " MNC: " + mnc); + } + + return { mcc: mcc, mnc: mnc}; + }, +}; + +/** + * Helper for ICC Contacts. + */ +function ICCContactHelperObject(aContext) { + this.context = aContext; +} +ICCContactHelperObject.prototype = { + context: null, + + /** + * Helper function to check DF_PHONEBOOK. + */ + hasDfPhoneBook: function(appType) { + switch (appType) { + case CARD_APPTYPE_SIM: + return false; + case CARD_APPTYPE_USIM: + return true; + case CARD_APPTYPE_RUIM: + let ICCUtilsHelper = this.context.ICCUtilsHelper; + return ICCUtilsHelper.isICCServiceAvailable("ENHANCED_PHONEBOOK"); + default: + return false; + } + }, + + /** + * Helper function to read ICC contacts. + * + * @param appType One of CARD_APPTYPE_*. + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readICCContacts: function(appType, contactType, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let ICCUtilsHelper = this.context.ICCUtilsHelper; + + switch (contactType) { + case GECKO_CARDCONTACT_TYPE_ADN: + if (!this.hasDfPhoneBook(appType)) { + ICCRecordHelper.readADNLike(ICC_EF_ADN, + (ICCUtilsHelper.isICCServiceAvailable("EXT1")) ? ICC_EF_EXT1 : null, + onsuccess, onerror); + } else { + this.readUSimContacts(onsuccess, onerror); + } + break; + case GECKO_CARDCONTACT_TYPE_FDN: + if (!ICCUtilsHelper.isICCServiceAvailable("FDN")) { + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + ICCRecordHelper.readADNLike(ICC_EF_FDN, + (ICCUtilsHelper.isICCServiceAvailable("EXT2")) ? ICC_EF_EXT2 : null, + onsuccess, onerror); + break; + case GECKO_CARDCONTACT_TYPE_SDN: + if (!ICCUtilsHelper.isICCServiceAvailable("SDN")) { + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + + ICCRecordHelper.readADNLike(ICC_EF_SDN, + (ICCUtilsHelper.isICCServiceAvailable("EXT3")) ? ICC_EF_EXT3 : null, + onsuccess, onerror); + break; + default: + if (DEBUG) { + this.context.debug("Unsupported contactType :" + contactType); + } + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + }, + + /** + * Helper function to find free contact record. + * + * @param appType One of CARD_APPTYPE_*. + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + findFreeICCContact: function(appType, contactType, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + switch (contactType) { + case GECKO_CARDCONTACT_TYPE_ADN: + if (!this.hasDfPhoneBook(appType)) { + ICCRecordHelper.findFreeRecordId(ICC_EF_ADN, onsuccess.bind(null, 0), onerror); + } else { + let gotPbrCb = function gotPbrCb(pbrs) { + this.findUSimFreeADNRecordId(pbrs, onsuccess, onerror); + }.bind(this); + + ICCRecordHelper.readPBR(gotPbrCb, onerror); + } + break; + case GECKO_CARDCONTACT_TYPE_FDN: + ICCRecordHelper.findFreeRecordId(ICC_EF_FDN, onsuccess.bind(null, 0), onerror); + break; + default: + if (DEBUG) { + this.context.debug("Unsupported contactType :" + contactType); + } + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + }, + + /** + * Cache the pbr index of the possible free record. + */ + _freePbrIndex: 0, + + /** + * Find free ADN record id in USIM. + * + * @param pbrs All Phonebook Reference Files read. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + findUSimFreeADNRecordId: function(pbrs, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + function callback(pbrIndex, recordId) { + // Assume other free records are probably in the same phonebook set. + this._freePbrIndex = pbrIndex; + onsuccess(pbrIndex, recordId); + } + + let nextPbrIndex = -1; + (function findFreeRecordId(pbrIndex) { + if (nextPbrIndex === this._freePbrIndex) { + // No free record found, reset the pbr index of free record. + this._freePbrIndex = 0; + if (DEBUG) { + this.context.debug(CONTACT_ERR_NO_FREE_RECORD_FOUND); + } + onerror(CONTACT_ERR_NO_FREE_RECORD_FOUND); + return; + } + + let pbr = pbrs[pbrIndex]; + nextPbrIndex = (pbrIndex + 1) % pbrs.length; + ICCRecordHelper.findFreeRecordId( + pbr.adn.fileId, + callback.bind(this, pbrIndex), + findFreeRecordId.bind(this, nextPbrIndex)); + }).call(this, this._freePbrIndex); + }, + + /** + * Helper function to add a new ICC contact. + * + * @param appType One of CARD_APPTYPE_*. + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param contact The contact will be added. + * @param pin2 PIN2 is required for FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + addICCContact: function(appType, contactType, contact, pin2, onsuccess, onerror) { + let foundFreeCb = (function foundFreeCb(pbrIndex, recordId) { + contact.pbrIndex = pbrIndex; + contact.recordId = recordId; + this.updateICCContact(appType, contactType, contact, pin2, onsuccess, onerror); + }).bind(this); + + // Find free record first. + this.findFreeICCContact(appType, contactType, foundFreeCb, onerror); + }, + + /** + * Helper function to update ICC contact. + * + * @param appType One of CARD_APPTYPE_*. + * @param contactType One of GECKO_CARDCONTACT_TYPE_*. + * @param contact The contact will be updated. + * @param pin2 PIN2 is required for FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateICCContact: function(appType, contactType, contact, pin2, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let ICCUtilsHelper = this.context.ICCUtilsHelper; + + let updateContactCb = (updatedContact) => { + updatedContact.pbrIndex = contact.pbrIndex; + updatedContact.recordId = contact.recordId; + onsuccess(updatedContact); + } + + switch (contactType) { + case GECKO_CARDCONTACT_TYPE_ADN: + if (!this.hasDfPhoneBook(appType)) { + if (ICCUtilsHelper.isICCServiceAvailable("EXT1")) { + this.updateADNLikeWithExtension(ICC_EF_ADN, ICC_EF_EXT1, + contact, null, + updateContactCb, onerror); + } else { + ICCRecordHelper.updateADNLike(ICC_EF_ADN, 0xff, + contact, null, + updateContactCb, onerror); + } + } else { + this.updateUSimContact(contact, updateContactCb, onerror); + } + break; + case GECKO_CARDCONTACT_TYPE_FDN: + if (!pin2) { + onerror(GECKO_ERROR_SIM_PIN2); + return; + } + if (!ICCUtilsHelper.isICCServiceAvailable("FDN")) { + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + if (ICCUtilsHelper.isICCServiceAvailable("EXT2")) { + this.updateADNLikeWithExtension(ICC_EF_FDN, ICC_EF_EXT2, + contact, pin2, + updateContactCb, onerror); + } else { + ICCRecordHelper.updateADNLike(ICC_EF_FDN, + 0xff, + contact, pin2, + updateContactCb, onerror); + } + break; + default: + if (DEBUG) { + this.context.debug("Unsupported contactType :" + contactType); + } + onerror(CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + break; + } + }, + + /** + * Read contacts from USIM. + * + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readUSimContacts: function(onsuccess, onerror) { + let gotPbrCb = function gotPbrCb(pbrs) { + this.readAllPhonebookSets(pbrs, onsuccess, onerror); + }.bind(this); + + this.context.ICCRecordHelper.readPBR(gotPbrCb, onerror); + }, + + /** + * Read all Phonebook sets. + * + * @param pbrs All Phonebook Reference Files read. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readAllPhonebookSets: function(pbrs, onsuccess, onerror) { + let allContacts = [], pbrIndex = 0; + let readPhonebook = function(contacts) { + if (contacts) { + allContacts = allContacts.concat(contacts); + } + + let cLen = contacts ? contacts.length : 0; + for (let i = 0; i < cLen; i++) { + contacts[i].pbrIndex = pbrIndex; + } + + pbrIndex++; + if (pbrIndex >= pbrs.length) { + if (onsuccess) { + onsuccess(allContacts); + } + return; + } + + this.readPhonebookSet(pbrs[pbrIndex], readPhonebook, onerror); + }.bind(this); + + this.readPhonebookSet(pbrs[pbrIndex], readPhonebook, onerror); + }, + + /** + * Read from Phonebook Reference File. + * + * @param pbr Phonebook Reference File to be read. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readPhonebookSet: function(pbr, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let gotAdnCb = function gotAdnCb(contacts) { + this.readSupportedPBRFields(pbr, contacts, onsuccess, onerror); + }.bind(this); + + ICCRecordHelper.readADNLike(pbr.adn.fileId, + (pbr.ext1) ? pbr.ext1.fileId : null, gotAdnCb, onerror); + }, + + /** + * Read supported Phonebook fields. + * + * @param pbr Phone Book Reference file. + * @param contacts Contacts stored on ICC. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readSupportedPBRFields: function(pbr, contacts, onsuccess, onerror) { + let fieldIndex = 0; + (function readField() { + let field = USIM_PBR_FIELDS[fieldIndex]; + fieldIndex += 1; + if (!field) { + if (onsuccess) { + onsuccess(contacts); + } + return; + } + + this.readPhonebookField(pbr, contacts, field, readField.bind(this), onerror); + }).call(this); + }, + + /** + * Read Phonebook field. + * + * @param pbr The phonebook reference file. + * @param contacts Contacts stored on ICC. + * @param field Phonebook field to be retrieved. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readPhonebookField: function(pbr, contacts, field, onsuccess, onerror) { + if (!pbr[field]) { + if (onsuccess) { + onsuccess(contacts); + } + return; + } + + (function doReadContactField(n) { + if (n >= contacts.length) { + // All contact's fields are read. + if (onsuccess) { + onsuccess(contacts); + } + return; + } + + // get n-th contact's field. + this.readContactField(pbr, contacts[n], field, + doReadContactField.bind(this, n + 1), onerror); + }).call(this, 0); + }, + + /** + * Read contact's field from USIM. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to get field. + * @param field Phonebook field to be retrieved. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + readContactField: function(pbr, contact, field, onsuccess, onerror) { + let gotRecordIdCb = function gotRecordIdCb(recordId) { + if (recordId == 0xff) { + if (onsuccess) { + onsuccess(); + } + return; + } + + let fileId = pbr[field].fileId; + let fileType = pbr[field].fileType; + let gotFieldCb = function gotFieldCb(value) { + if (value) { + // Move anr0 anr1,.. into anr[]. + if (field.startsWith(USIM_PBR_ANR)) { + if (!contact[USIM_PBR_ANR]) { + contact[USIM_PBR_ANR] = []; + } + contact[USIM_PBR_ANR].push(value); + } else { + contact[field] = value; + } + } + + if (onsuccess) { + onsuccess(); + } + }.bind(this); + + let ICCRecordHelper = this.context.ICCRecordHelper; + // Detect EF to be read, for anr, it could have anr0, anr1,... + let ef = field.startsWith(USIM_PBR_ANR) ? USIM_PBR_ANR : field; + switch (ef) { + case USIM_PBR_EMAIL: + ICCRecordHelper.readEmail(fileId, fileType, recordId, gotFieldCb, onerror); + break; + case USIM_PBR_ANR: + ICCRecordHelper.readANR(fileId, fileType, recordId, gotFieldCb, onerror); + break; + default: + if (DEBUG) { + this.context.debug("Unsupported field :" + field); + } + onerror(CONTACT_ERR_FIELD_NOT_SUPPORTED); + break; + } + }.bind(this); + + this.getContactFieldRecordId(pbr, contact, field, gotRecordIdCb, onerror); + }, + + /** + * Get the recordId. + * + * If the fileType of field is ICC_USIM_TYPE1_TAG, use corresponding ADN recordId. + * otherwise get the recordId from IAP. + * + * @see TS 131.102, clause 4.4.2.2 + * + * @param pbr The phonebook reference file. + * @param contact The contact will be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + getContactFieldRecordId: function(pbr, contact, field, onsuccess, onerror) { + if (pbr[field].fileType == ICC_USIM_TYPE1_TAG) { + // If the file type is ICC_USIM_TYPE1_TAG, use corresponding ADN recordId. + if (onsuccess) { + onsuccess(contact.recordId); + } + } else if (pbr[field].fileType == ICC_USIM_TYPE2_TAG) { + // If the file type is ICC_USIM_TYPE2_TAG, the recordId shall be got from IAP. + let gotIapCb = function gotIapCb(iap) { + let indexInIAP = pbr[field].indexInIAP; + let recordId = iap[indexInIAP]; + + if (onsuccess) { + onsuccess(recordId); + } + }.bind(this); + + this.context.ICCRecordHelper.readIAP(pbr.iap.fileId, contact.recordId, + gotIapCb, onerror); + } else { + if (DEBUG) { + this.context.debug("USIM PBR files in Type 3 format are not supported."); + } + onerror(CONTACT_ERR_REQUEST_NOT_SUPPORTED); + } + }, + + /** + * Update USIM contact. + * + * @param contact The contact will be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateUSimContact: function(contact, onsuccess, onerror) { + let gotPbrCb = function gotPbrCb(pbrs) { + let pbr = pbrs[contact.pbrIndex]; + if (!pbr) { + if (DEBUG) { + this.context.debug(CONTACT_ERR_CANNOT_ACCESS_PHONEBOOK); + } + onerror(CONTACT_ERR_CANNOT_ACCESS_PHONEBOOK); + return; + } + this.updatePhonebookSet(pbr, contact, onsuccess, onerror); + }.bind(this); + + this.context.ICCRecordHelper.readPBR(gotPbrCb, onerror); + }, + + /** + * Update fields in Phonebook Reference File. + * + * @param pbr Phonebook Reference File to be read. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updatePhonebookSet: function(pbr, contact, onsuccess, onerror) { + let updateAdnCb = function(updatedContact) { + this.updateSupportedPBRFields(pbr, contact, (updatedContactField) => { + onsuccess(Object.assign(updatedContact, updatedContactField)); + }, onerror); + }.bind(this); + + if (pbr.ext1) { + this.updateADNLikeWithExtension(pbr.adn.fileId, pbr.ext1.fileId, + contact, null, updateAdnCb, onerror); + } else { + this.context.ICCRecordHelper.updateADNLike(pbr.adn.fileId, 0xff, contact, + null, updateAdnCb, onerror); + } + }, + + /** + * Update supported Phonebook fields. + * + * @param pbr Phone Book Reference file. + * @param contact Contact to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateSupportedPBRFields: function(pbr, contact, onsuccess, onerror) { + let fieldIndex = 0; + let contactField = {}; + + (function updateField() { + let field = USIM_PBR_FIELDS[fieldIndex]; + fieldIndex += 1; + + if (!field) { + if (onsuccess) { + onsuccess(contactField); + } + return; + } + + // Check if PBR has this field. + if (!pbr[field]) { + updateField.call(this); + return; + } + + this.updateContactField(pbr, contact, field, (fieldEntry) => { + contactField = Object.assign(contactField, fieldEntry); + updateField.call(this); + }, (errorMsg) => { + // Bug 1194149, there are some sim cards without sufficient + // Type 2 USIM contact fields record. We allow user continue + // importing contacts. + if (errorMsg === CONTACT_ERR_NO_FREE_RECORD_FOUND) { + updateField.call(this); + return; + } + onerror(errorMsg); + }); + }).call(this); + }, + + /** + * Update contact's field from USIM. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to be updated. + * @param field Phonebook field to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateContactField: function(pbr, contact, field, onsuccess, onerror) { + if (pbr[field].fileType === ICC_USIM_TYPE1_TAG) { + this.updateContactFieldType1(pbr, contact, field, onsuccess, onerror); + } else if (pbr[field].fileType === ICC_USIM_TYPE2_TAG) { + this.updateContactFieldType2(pbr, contact, field, onsuccess, onerror); + } else { + if (DEBUG) { + this.context.debug("USIM PBR files in Type 3 format are not supported."); + } + onerror(CONTACT_ERR_REQUEST_NOT_SUPPORTED); + } + }, + + /** + * Update Type 1 USIM contact fields. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to be updated. + * @param field Phonebook field to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateContactFieldType1: function(pbr, contact, field, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + if (field === USIM_PBR_EMAIL) { + ICCRecordHelper.updateEmail(pbr, contact.recordId, contact.email, null, + (updatedEmail) => { + onsuccess({email: updatedEmail}); + }, onerror); + } else if (field === USIM_PBR_ANR0) { + let anr = Array.isArray(contact.anr) ? contact.anr[0] : null; + ICCRecordHelper.updateANR(pbr, contact.recordId, anr, null, + (updatedANR) => { + // ANR could have multiple files. If we support more than one anr, + // we will save it as anr0, anr1,...etc. + onsuccess((updatedANR) ? {anr: [updatedANR]} : null); + }, onerror); + } else { + if (DEBUG) { + this.context.debug("Unsupported field :" + field); + } + onerror(CONTACT_ERR_FIELD_NOT_SUPPORTED); + } + }, + + /** + * Update Type 2 USIM contact fields. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to be updated. + * @param field Phonebook field to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateContactFieldType2: function(pbr, contact, field, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + // Case 1 : EF_IAP[adnRecordId] doesn't have a value(0xff) + // Find a free recordId for EF_field + // Update field with that free recordId. + // Update IAP. + // + // Case 2: EF_IAP[adnRecordId] has a value + // update EF_field[iap[field.indexInIAP]] + + let gotIapCb = function gotIapCb(iap) { + let recordId = iap[pbr[field].indexInIAP]; + if (recordId === 0xff) { + // If the value in IAP[index] is 0xff, which means the contact stored on + // the SIM doesn't have the additional attribute (email or anr). + // So if the contact to be updated doesn't have the attribute either, + // we don't have to update it. + if ((field === USIM_PBR_EMAIL && contact.email) || + (field === USIM_PBR_ANR0 && + (Array.isArray(contact.anr) && contact.anr[0]))) { + // Case 1. + this.addContactFieldType2(pbr, contact, field, onsuccess, onerror); + } else { + if (onsuccess) { + onsuccess(); + } + } + return; + } + + // Case 2. + if (field === USIM_PBR_EMAIL) { + ICCRecordHelper.updateEmail(pbr, recordId, contact.email, contact.recordId, + (updatedEmail) => { + onsuccess({email: updatedEmail}); + }, onerror); + } else if (field === USIM_PBR_ANR0) { + let anr = Array.isArray(contact.anr) ? contact.anr[0] : null; + ICCRecordHelper.updateANR(pbr, recordId, anr, contact.recordId, + (updatedANR) => { + // ANR could have multiple files. If we support more than one anr, + // we will save it as anr0, anr1,...etc. + onsuccess((updatedANR) ? {anr: [updatedANR]} : null); + }, onerror); + } else { + if (DEBUG) { + this.context.debug("Unsupported field :" + field); + } + onerror(CONTACT_ERR_FIELD_NOT_SUPPORTED); + } + + }.bind(this); + + ICCRecordHelper.readIAP(pbr.iap.fileId, contact.recordId, gotIapCb, onerror); + }, + + /** + * Add Type 2 USIM contact fields. + * + * @param pbr The phonebook reference file. + * @param contact The contact needs to be updated. + * @param field Phonebook field to be updated. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + addContactFieldType2: function(pbr, contact, field, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let successCb = function successCb(recordId) { + + let updateCb = function updateCb(contactField) { + this.updateContactFieldIndexInIAP(pbr, contact.recordId, field, recordId, () => { + onsuccess(contactField); + }, onerror); + }.bind(this); + + if (field === USIM_PBR_EMAIL) { + ICCRecordHelper.updateEmail(pbr, recordId, contact.email, contact.recordId, + (updatedEmail) => { + updateCb({email: updatedEmail}); + }, onerror); + } else if (field === USIM_PBR_ANR0) { + ICCRecordHelper.updateANR(pbr, recordId, contact.anr[0], contact.recordId, + (updatedANR) => { + // ANR could have multiple files. If we support more than one anr, + // we will save it as anr0, anr1,...etc. + updateCb((updatedANR) ? {anr: [updatedANR]} : null); + }, onerror); + } + }.bind(this); + + let errorCb = function errorCb(errorMsg) { + if (DEBUG) { + this.context.debug(errorMsg + " USIM field " + field); + } + onerror(errorMsg); + }.bind(this); + + ICCRecordHelper.findFreeRecordId(pbr[field].fileId, successCb, errorCb); + }, + + /** + * Update IAP value. + * + * @param pbr The phonebook reference file. + * @param recordNumber The record identifier of EF_IAP. + * @param field Phonebook field. + * @param value The value of 'field' in IAP. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + * + */ + updateContactFieldIndexInIAP: function(pbr, recordNumber, field, value, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + + let gotIAPCb = function gotIAPCb(iap) { + iap[pbr[field].indexInIAP] = value; + ICCRecordHelper.updateIAP(pbr.iap.fileId, recordNumber, iap, onsuccess, onerror); + }.bind(this); + ICCRecordHelper.readIAP(pbr.iap.fileId, recordNumber, gotIAPCb, onerror); + }, + + /** + * Update ICC ADN like EFs with Extension, like EF_ADN, EF_FDN. + * + * @param fileId EF id of the ADN or FDN. + * @param extFileId EF id of the EXT. + * @param contact The contact will be updated. (Shall have recordId property) + * @param pin2 PIN2 is required when updating ICC_EF_FDN. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + updateADNLikeWithExtension: function(fileId, extFileId, contact, pin2, onsuccess, onerror) { + let ICCRecordHelper = this.context.ICCRecordHelper; + let extNumber; + + if (contact.number) { + let numStart = contact.number[0] == "+" ? 1 : 0; + let number = contact.number.substring(0, numStart) + + this.context.GsmPDUHelper.stringToExtendedBcd( + contact.number.substring(numStart)); + extNumber = number.substr(numStart + ADN_MAX_NUMBER_DIGITS, + EXT_MAX_NUMBER_DIGITS); + } + + ICCRecordHelper.getADNLikeExtensionRecordNumber(fileId, contact.recordId, + (extRecordNumber) => { + let updateADNLike = (extRecordNumber) => { + ICCRecordHelper.updateADNLike(fileId, extRecordNumber, contact, + pin2, (updatedContact) => { + if (extNumber && extRecordNumber != 0xff) { + updatedContact.number = updatedContact.number.concat(extNumber); + } + onsuccess(updatedContact); + }, onerror); + }; + + let updateExtension = (extRecordNumber) => { + ICCRecordHelper.updateExtension(extFileId, extRecordNumber, extNumber, + () => updateADNLike(extRecordNumber), + () => updateADNLike(0xff)); + }; + + if (extNumber) { + if (extRecordNumber != 0xff) { + updateExtension(extRecordNumber); + return; + } + + ICCRecordHelper.findFreeRecordId(extFileId, + (extRecordNumber) => updateExtension(extRecordNumber), + (errorMsg) => { + if (DEBUG) { + this.context.debug("Couldn't find free extension record Id for " + extFileId + ": " + errorMsg); + } + updateADNLike(0xff); + }); + return; + } + + if (extRecordNumber != 0xff) { + ICCRecordHelper.cleanEFRecord(extFileId, extRecordNumber, + () => updateADNLike(0xff), onerror); + return; + } + + updateADNLike(0xff); + }, onerror); + }, +}; + +function IconLoaderObject(aContext) { + this.context = aContext; +} +IconLoaderObject.prototype = { + context: null, + + /** + * Load icons. + * + * @param recordNumbers Array of the record identifiers of EF_IMG. + * @param onsuccess Callback to be called when success. + * @param onerror Callback to be called when error. + */ + loadIcons: function(recordNumbers, onsuccess, onerror) { + if (!recordNumbers || !recordNumbers.length) { + if (onerror) { + onerror(); + } + return; + } + + this._start({ + recordNumbers: recordNumbers, + onsuccess: onsuccess, + onerror: onerror}); + }, + + _start: function(options) { + let callback = (function(icons) { + if (!options.icons) { + options.icons = []; + } + for (let i = 0; i < icons.length; i++) { + icons[i] = this._parseRawData(icons[i]); + } + options.icons[options.currentRecordIndex] = icons; + options.currentRecordIndex++; + + let recordNumbers = options.recordNumbers; + if (options.currentRecordIndex < recordNumbers.length) { + let recordNumber = recordNumbers[options.currentRecordIndex]; + this.context.SimRecordHelper.readIMG(recordNumber, + callback, + options.onerror); + } else { + if (options.onsuccess) { + options.onsuccess(options.icons); + } + } + }).bind(this); + + options.currentRecordIndex = 0; + this.context.SimRecordHelper.readIMG(options.recordNumbers[0], + callback, + options.onerror); + }, + + _parseRawData: function(rawData) { + let codingScheme = rawData.codingScheme; + + switch (codingScheme) { + case ICC_IMG_CODING_SCHEME_BASIC: + return this._decodeBasicImage(rawData.width, rawData.height, rawData.body); + + case ICC_IMG_CODING_SCHEME_COLOR: + case ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY: + return this._decodeColorImage(codingScheme, + rawData.width, rawData.height, + rawData.bitsPerImgPoint, + rawData.numOfClutEntries, + rawData.clut, rawData.body); + } + + return null; + }, + + _decodeBasicImage: function(width, height, body) { + let numOfPixels = width * height; + let pixelIndex = 0; + let currentByteIndex = 0; + let currentByte = 0x00; + + const BLACK = 0x000000FF; + const WHITE = 0xFFFFFFFF; + + let pixels = []; + while (pixelIndex < numOfPixels) { + // Reassign data and index for every byte (8 bits). + if (pixelIndex % 8 == 0) { + currentByte = body[currentByteIndex++]; + } + let bit = (currentByte >> (7 - (pixelIndex % 8))) & 0x01; + pixels[pixelIndex++] = bit ? WHITE : BLACK; + } + + return {pixels: pixels, + codingScheme: GECKO_IMG_CODING_SCHEME_BASIC, + width: width, + height: height}; + }, + + _decodeColorImage: function(codingScheme, width, height, bitsPerImgPoint, + numOfClutEntries, clut, body) { + let mask = 0xff >> (8 - bitsPerImgPoint); + let bitsStartOffset = 8 - bitsPerImgPoint; + let bitIndex = bitsStartOffset; + let numOfPixels = width * height; + let pixelIndex = 0; + let currentByteIndex = 0; + let currentByte = body[currentByteIndex++]; + + let pixels = []; + while (pixelIndex < numOfPixels) { + // Reassign data and index for every byte (8 bits). + if (bitIndex < 0) { + currentByte = body[currentByteIndex++]; + bitIndex = bitsStartOffset; + } + let clutEntry = ((currentByte >> bitIndex) & mask); + let clutIndex = clutEntry * ICC_CLUT_ENTRY_SIZE; + let alpha = codingScheme == ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY && + clutEntry == numOfClutEntries - 1; + pixels[pixelIndex++] = alpha ? 0x00 + : (clut[clutIndex] << 24 | + clut[clutIndex + 1] << 16 | + clut[clutIndex + 2] << 8 | + 0xFF) >>> 0; + bitIndex -= bitsPerImgPoint; + } + + return {pixels: pixels, + codingScheme: ICC_IMG_CODING_SCHEME_TO_GECKO[codingScheme], + width: width, + height: height}; + }, +}; + +/** + * Global stuff. + */ + +function Context(aClientId) { + this.clientId = aClientId; + + this.Buf = new BufObject(this); + this.RIL = new RilObject(this); + this.RIL.initRILState(); +} +Context.prototype = { + clientId: null, + Buf: null, + RIL: null, + + debug: function(aMessage) { + GLOBAL.debug("[" + this.clientId + "] " + aMessage); + } +}; + +(function() { + let lazySymbols = [ + "BerTlvHelper", "BitBufferHelper", "CdmaPDUHelper", + "ComprehensionTlvHelper", "GsmPDUHelper", "ICCContactHelper", + "ICCFileHelper", "ICCIOHelper", "ICCPDUHelper", "ICCRecordHelper", + "ICCUtilsHelper", "RuimRecordHelper", "SimRecordHelper", + "StkCommandParamsFactory", "StkProactiveCmdHelper", "IconLoader", + ]; + + for (let i = 0; i < lazySymbols.length; i++) { + let symbol = lazySymbols[i]; + Object.defineProperty(Context.prototype, symbol, { + get: function() { + let real = new GLOBAL[symbol + "Object"](this); + Object.defineProperty(this, symbol, { + value: real, + enumerable: true + }); + return real; + }, + configurable: true, + enumerable: true + }); + } +})(); + +var ContextPool = { + _contexts: [], + + handleRilMessage: function(aClientId, aUint8Array) { + let context = this._contexts[aClientId]; + context.Buf.processIncoming(aUint8Array); + }, + + handleChromeMessage: function(aMessage) { + let clientId = aMessage.rilMessageClientId; + if (clientId != null) { + let context = this._contexts[clientId]; + context.RIL.handleChromeMessage(aMessage); + return; + } + + if (DEBUG) debug("Received global chrome message " + JSON.stringify(aMessage)); + let method = this[aMessage.rilMessageType]; + if (typeof method != "function") { + if (DEBUG) { + debug("Don't know what to do"); + } + return; + } + method.call(this, aMessage); + }, + + setInitialOptions: function(aOptions) { + DEBUG = DEBUG_WORKER || aOptions.debug; + + let quirks = aOptions.quirks; + RILQUIRKS_CALLSTATE_EXTRA_UINT32 = quirks.callstateExtraUint32; + RILQUIRKS_REQUEST_USE_DIAL_EMERGENCY_CALL = quirks.requestUseDialEmergencyCall; + RILQUIRKS_SIM_APP_STATE_EXTRA_FIELDS = quirks.simAppStateExtraFields; + RILQUIRKS_EXTRA_UINT32_2ND_CALL = quirks.extraUint2ndCall; + RILQUIRKS_HAVE_QUERY_ICC_LOCK_RETRY_COUNT = quirks.haveQueryIccLockRetryCount; + RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD = quirks.sendStkProfileDownload; + RILQUIRKS_DATA_REGISTRATION_ON_DEMAND = quirks.dataRegistrationOnDemand; + RILQUIRKS_SUBSCRIPTION_CONTROL = quirks.subscriptionControl; + RILQUIRKS_SIGNAL_EXTRA_INT32 = quirks.signalExtraInt; + RILQUIRKS_AVAILABLE_NETWORKS_EXTRA_STRING = quirks.availableNetworkExtraStr; + RILQUIRKS_SMSC_ADDRESS_FORMAT = quirks.smscAddressFormat; + }, + + setDebugFlag: function(aOptions) { + DEBUG = DEBUG_WORKER || aOptions.debug; + }, + + registerClient: function(aOptions) { + let clientId = aOptions.clientId; + this._contexts[clientId] = new Context(clientId); + }, +}; + +function onRILMessage(aClientId, aUint8Array) { + ContextPool.handleRilMessage(aClientId, aUint8Array); +} + +onmessage = function onmessage(event) { + ContextPool.handleChromeMessage(event.data); +}; + +onerror = function onerror(event) { + if (DEBUG) debug("onerror" + event.message + "\n"); +}; diff --git a/dom/system/gonk/ril_worker_buf_object.js b/dom/system/gonk/ril_worker_buf_object.js new file mode 100644 index 000000000..70018c5b2 --- /dev/null +++ b/dom/system/gonk/ril_worker_buf_object.js @@ -0,0 +1,168 @@ +/* global require */ +/* global DEBUG, DEBUG_WORKER */ +/* global RESPONSE_TYPE_SOLICITED */ +/* global RESPONSE_TYPE_UNSOLICITED */ + +"use strict"; + +/** + * This is a specialized worker buffer for the Parcel protocol. + * + * NOTE: To prevent including/importing twice, this file should be included + * in a file which already includes 'ril_consts.js' and 'require.js'. + */ +(function(exports) { + + // Set to true in ril_consts.js to see debug messages + let DEBUG = DEBUG_WORKER; + // Need to inherit it. + let Buf = require("resource://gre/modules/workers/worker_buf.js").Buf; + + let BufObject = function(aContext) { + this.context = aContext; + // This gets incremented each time we send out a parcel. + this.mToken = 1; + // Maps tokens we send out with requests to the request type, so that + // when we get a response parcel back, we know what request it was for. + this.mTokenRequestMap = new Map(); + // This is because the underlying 'Buf' is still using the 'init' pattern, so + // this derived one needs to invoke it. + // Using 'apply' style to mark it's a parent method calling explicitly. + Buf._init.apply(this); + + // Remapping the request type to different values based on RIL version. + // We only have to do this for SUBSCRIPTION right now, so I just make it + // simple. A generic logic or structure could be discussed if we have more + // use cases, especially the cases from different partners. + this._requestMap = {}; + // RIL version 8. + // For the CAF's proprietary parcels. Please see + // https://www.codeaurora.org/cgit/quic/la/platform/hardware/ril/tree/include/telephony/ril.h?h=b2g_jb_3.2 + let map = {}; + map[REQUEST_SET_UICC_SUBSCRIPTION] = 114; + map[REQUEST_SET_DATA_SUBSCRIPTION] = 115; + this._requestMap[8] = map; + // RIL version 9. + // For the CAF's proprietary parcels. Please see + // https://www.codeaurora.org/cgit/quic/la/platform/hardware/ril/tree/include/telephony/ril.h?h=b2g_kk_3.5 + map = {}; + map[REQUEST_SET_UICC_SUBSCRIPTION] = 115; + map[REQUEST_SET_DATA_SUBSCRIPTION] = 116; + this._requestMap[9] = map; + }; + + /** + * "inherit" the basic worker buffer. + */ + BufObject.prototype = Object.create(Buf); + + /** + * Process one parcel. + */ + BufObject.prototype.processParcel = function() { + let responseType = this.readInt32(); + + let requestType, options; + if (responseType == RESPONSE_TYPE_SOLICITED) { + let token = this.readInt32(); + let error = this.readInt32(); + + options = this.mTokenRequestMap.get(token); + if (!options) { + if (DEBUG) { + this.context.debug("Suspicious uninvited request found: " + + token + ". Ignored!"); + } + return; + } + + this.mTokenRequestMap.delete(token); + requestType = options.rilRequestType; + + if (error !== ERROR_SUCCESS) { + options.errorMsg = RIL_ERROR_TO_GECKO_ERROR[error] || + GECKO_ERROR_UNSPECIFIED_ERROR; + } + if (DEBUG) { + this.context.debug("Solicited response for request type " + requestType + + ", token " + token + ", error " + error); + } + } else if (responseType == RESPONSE_TYPE_UNSOLICITED) { + requestType = this.readInt32(); + if (DEBUG) { + this.context.debug("Unsolicited response for request type " + requestType); + } + } else { + if (DEBUG) { + this.context.debug("Unknown response type: " + responseType); + } + return; + } + + this.context.RIL.handleParcel(requestType, this.readAvailable, options); + }; + + /** + * Start a new outgoing parcel. + * + * @param type + * Integer specifying the request type. + * @param options [optional] + * Object containing information about the request, e.g. the + * original main thread message object that led to the RIL request. + */ + BufObject.prototype.newParcel = function(type, options) { + if (DEBUG) { + this.context.debug("New outgoing parcel of type " + type); + } + + // We're going to leave room for the parcel size at the beginning. + this.outgoingIndex = this.PARCEL_SIZE_SIZE; + this.writeInt32(this._reMapRequestType(type)); + this.writeInt32(this.mToken); + + if (!options) { + options = {}; + } + options.rilRequestType = type; + this.mTokenRequestMap.set(this.mToken, options); + this.mToken++; + return this.mToken; + }; + + BufObject.prototype.simpleRequest = function(type, options) { + this.newParcel(type, options); + this.sendParcel(); + }; + + BufObject.prototype.onSendParcel = function(parcel) { + self.postRILMessage(this.context.clientId, parcel); + }; + + /** + * Remapping the request type to different values based on RIL version. + * We only have to do this for SUBSCRIPTION right now, so I just make it + * simple. A generic logic or structure could be discussed if we have more + * use cases, especially the cases from different partners. + */ + BufObject.prototype._reMapRequestType = function(type) { + for (let version in this._requestMap) { + if (this.context.RIL.version <= version) { + let newType = this._requestMap[version][type]; + if (newType) { + if (DEBUG) { + this.context.debug("Remap request type to " + newType); + } + return newType; + } + } + } + return type; + }; + + // Before we make sure to form it as a module would not add extra + // overhead of module loading, we need to define it in this way + // rather than 'module.exports' it as a module component. + exports.BufObject = BufObject; +})(self); // in worker self is the global + diff --git a/dom/system/gonk/ril_worker_telephony_request_queue.js b/dom/system/gonk/ril_worker_telephony_request_queue.js new file mode 100644 index 000000000..4dba7a42f --- /dev/null +++ b/dom/system/gonk/ril_worker_telephony_request_queue.js @@ -0,0 +1,157 @@ +/* global DEBUG, DEBUG_WORKER */ +/* global REQUEST_GET_CURRENT_CALLS */ +/* global REQUEST_ANSWER, REQUEST_CONFERENCE, REQUEST_DIAL */ +/* global REQUEST_DIAL_EMERGENCY_CALL, REQUEST_HANGUP */ +/* global REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND */ +/* global REQUEST_HANGUP_WAITING_OR_BACKGROUND */ +/* global REQUEST_SEPARATE_CONNECTION */ +/* global REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, REQUEST_UDUB */ + +"use strict"; + +(function(exports) { + + const TELEPHONY_REQUESTS = [ + REQUEST_GET_CURRENT_CALLS, + REQUEST_ANSWER, + REQUEST_CONFERENCE, + REQUEST_DIAL, + REQUEST_DIAL_EMERGENCY_CALL, + REQUEST_HANGUP, + REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND, + REQUEST_HANGUP_WAITING_OR_BACKGROUND, + REQUEST_SEPARATE_CONNECTION, + REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, + REQUEST_UDUB + ]; + + // Set to true in ril_consts.js to see debug messages + let DEBUG = DEBUG_WORKER; + + /** + * Queue entry; only used in the queue. + */ + let TelephonyRequestEntry = function(request, callback) { + this.request = request; + this.callback = callback; + }; + + let TelephonyRequestQueue = function(ril) { + this.ril = ril; + this.currentQueue = null; // Point to the current running queue. + + this.queryQueue = []; + this.controlQueue = []; + }; + + TelephonyRequestQueue.prototype._getQueue = function(request) { + return (request === REQUEST_GET_CURRENT_CALLS) ? this.queryQueue + : this.controlQueue; + }; + + TelephonyRequestQueue.prototype._getAnotherQueue = function(queue) { + return (this.queryQueue === queue) ? this.controlQueue : this.queryQueue; + }; + + TelephonyRequestQueue.prototype._find = function(queue, request) { + for (let i = 0; i < queue.length; ++i) { + if (queue[i].request === request) { + return i; + } + } + return -1; + }; + + TelephonyRequestQueue.prototype._startQueue = function(queue) { + if (queue.length === 0) { + return; + } + + // We only need to keep one entry for queryQueue. + if (queue === this.queryQueue) { + queue.splice(1, queue.length - 1); + } + + this.currentQueue = queue; + for (let entry of queue) { + this._executeEntry(entry); + } + }; + + TelephonyRequestQueue.prototype._executeEntry = function(entry) { + if (DEBUG) { + this.debug("execute " + this._getRequestName(entry.request)); + } + entry.callback(); + }; + + TelephonyRequestQueue.prototype._getRequestName = function(request) { + let method = this.ril[request]; + return (typeof method === 'function') ? method.name : ""; + }; + + TelephonyRequestQueue.prototype.debug = function(msg) { + this.ril.context.debug("[TeleQ] " + msg); + }; + + TelephonyRequestQueue.prototype.isValidRequest = function(request) { + return TELEPHONY_REQUESTS.indexOf(request) !== -1; + }; + + TelephonyRequestQueue.prototype.push = function(request, callback) { + if (!this.isValidRequest(request)) { + if (DEBUG) { + this.debug("Error: " + this._getRequestName(request) + + " is not a telephony request"); + } + return; + } + + if (DEBUG) { + this.debug("push " + this._getRequestName(request)); + } + let entry = new TelephonyRequestEntry(request, callback); + let queue = this._getQueue(request); + queue.push(entry); + + // Try to run the request. + if (this.currentQueue === queue) { + this._executeEntry(entry); + } else if (!this.currentQueue) { + this._startQueue(queue); + } + }; + + TelephonyRequestQueue.prototype.pop = function(request) { + if (!this.isValidRequest(request)) { + if (DEBUG) { + this.debug("Error: " + this._getRequestName(request) + + " is not a telephony request"); + } + return; + } + + if (DEBUG) { + this.debug("pop " + this._getRequestName(request)); + } + let queue = this._getQueue(request); + let index = this._find(queue, request); + if (index === -1) { + throw new Error("Cannot find the request in telephonyRequestQueue."); + } else { + queue.splice(index, 1); + } + + if (queue.length === 0) { + this.currentQueue = null; + this._startQueue(this._getAnotherQueue(queue)); + } + }; + + + // Before we make sure to form it as a module would not add extra + // overhead of module loading, we need to define it in this way + // rather than 'module.exports' it as a module component. + exports.TelephonyRequestQueue = TelephonyRequestQueue; +})(self); // in worker self is the global + diff --git a/dom/system/gonk/systemlibs.js b/dom/system/gonk/systemlibs.js new file mode 100644 index 000000000..a27b30e20 --- /dev/null +++ b/dom/system/gonk/systemlibs.js @@ -0,0 +1,201 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * 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. + */ + +if (!this.ctypes) { + // We're likely being loaded as a JSM. + this.EXPORTED_SYMBOLS = [ "libcutils", "netHelpers" ]; + Components.utils.import("resource://gre/modules/ctypes.jsm"); +} + +const SYSTEM_PROPERTY_KEY_MAX = 32; +const SYSTEM_PROPERTY_VALUE_MAX = 92; + +// We leave this as 'undefined' instead of setting it to 'false'. That +// way a file that includes us can have it defined already without us +// overriding the value here. +var DEBUG; + +/** + * Expose some system-level functions. + */ +this.libcutils = (function() { + let lib; + try { + lib = ctypes.open("libcutils.so"); + } catch(ex) { + // Return a fallback option in case libcutils.so isn't present (e.g. + // when building Firefox with MOZ_B2G_RIL. + if (DEBUG) { + dump("Could not load libcutils.so. Using fake propdb.\n"); + } + let fake_propdb = Object.create(null); + return { + property_get: function(key, defaultValue) { + if (key in fake_propdb) { + return fake_propdb[key]; + } + return defaultValue === undefined ? null : defaultValue; + }, + property_set: function(key, value) { + fake_propdb[key] = value; + } + }; + } + + let c_property_get = lib.declare("property_get", ctypes.default_abi, + ctypes.int, // return value: length + ctypes.char.ptr, // key + ctypes.char.ptr, // value + ctypes.char.ptr); // default + let c_property_set = lib.declare("property_set", ctypes.default_abi, + ctypes.int, // return value: success + ctypes.char.ptr, // key + ctypes.char.ptr); // value + let c_value_buf = ctypes.char.array(SYSTEM_PROPERTY_VALUE_MAX)(); + + return { + + /** + * Get a system property. + * + * @param key + * Name of the property + * @param defaultValue [optional] + * Default value to return if the property isn't set (default: null) + */ + property_get: function(key, defaultValue) { + if (defaultValue === undefined) { + defaultValue = null; + } + c_property_get(key, c_value_buf, defaultValue); + return c_value_buf.readString(); + }, + + /** + * Set a system property + * + * @param key + * Name of the property + * @param value + * Value to set the property to. + */ + property_set: function(key, value) { + let rv = c_property_set(key, value); + if (rv) { + throw Error('libcutils.property_set("' + key + '", "' + value + + '") failed with error ' + rv); + } + } + + }; +})(); + +/** + * Helpers for conversions. + */ +this.netHelpers = { + + /** + * Swap byte orders for 32-bit value + */ + swap32: function(n) { + return (((n >> 24) & 0xFF) << 0) | + (((n >> 16) & 0xFF) << 8) | + (((n >> 8) & 0xFF) << 16) | + (((n >> 0) & 0xFF) << 24); + }, + + /** + * Convert network byte order to host byte order + * Note: Assume that the system is little endian + */ + ntohl: function(n) { + return this.swap32(n); + }, + + /** + * Convert host byte order to network byte order + * Note: Assume that the system is little endian + */ + htonl: function(n) { + return this.swap32(n); + }, + + /** + * Convert integer representation of an IP address to the string + * representation. + * + * @param ip + * IP address in number format. + */ + ipToString: function(ip) { + return ((ip >> 0) & 0xFF) + "." + + ((ip >> 8) & 0xFF) + "." + + ((ip >> 16) & 0xFF) + "." + + ((ip >> 24) & 0xFF); + }, + + /** + * Convert string representation of an IP address to the integer + * representation (network byte order). + * + * @param string + * String containing the IP address. + */ + stringToIP: function(string) { + if (!string) { + return null; + } + let ip = 0; + let start, end = -1; + for (let i = 0; i < 4; i++) { + start = end + 1; + end = string.indexOf(".", start); + if (end == -1) { + end = string.length; + } + let num = parseInt(string.slice(start, end), 10); + if (isNaN(num)) { + return null; + } + ip |= num << (i * 8); + } + return ip; + }, + + /** + * Make a subnet mask. + */ + makeMask: function(len) { + let mask = 0; + for (let i = 0; i < len; ++i) { + mask |= (0x80000000 >> i); + } + return this.ntohl(mask); + }, + + /** + * Get Mask length from given mask address + */ + getMaskLength: function(mask) { + let len = 0; + let netmask = this.ntohl(mask); + while (netmask & 0x80000000) { + len++; + netmask = netmask << 1; + } + return len; + } +}; diff --git a/dom/system/gonk/tests/header_helpers.js b/dom/system/gonk/tests/header_helpers.js new file mode 100644 index 000000000..8d1144f75 --- /dev/null +++ b/dom/system/gonk/tests/header_helpers.js @@ -0,0 +1,217 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + + +var subscriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader); + +/** + * Start a new RIL worker. + * + * @param custom_ns + * Namespace with symbols to be injected into the new worker + * namespace. + * + * @return an object that represents the worker's namespace. + * + * @note that this does not start an actual worker thread. The worker + * is executed on the main thread, within a separate namespace object. + */ +function newWorker(custom_ns) { + let worker_ns = { + importScripts: function() { + Array.slice(arguments).forEach(function(script) { + if (!script.startsWith("resource:")) { + script = "resource://gre/modules/" + script; + } + subscriptLoader.loadSubScript(script, this); + }, this); + }, + + postRILMessage: function(message) { + }, + + postMessage: function(message) { + }, + + // Define these variables inside the worker scope so ES5 strict mode + // doesn't flip out. + onmessage: undefined, + onerror: undefined, + + DEBUG: true + }; + // The 'self' variable in a worker points to the worker's own namespace. + worker_ns.self = worker_ns; + + // Copy the custom definitions over. + for (let key in custom_ns) { + worker_ns[key] = custom_ns[key]; + } + + // fake require() for toolkit/components/workerloader/require.js + let require = (function() { + return function require(script) { + worker_ns.module = {}; + worker_ns.importScripts(script); + return worker_ns; + } + })(); + + Object.freeze(require); + Object.defineProperty(worker_ns, "require", { + value: require, + enumerable: true, + configurable: false + }); + + // Load the RIL worker itself. + worker_ns.importScripts("ril_worker.js"); + + // Register at least one client. + worker_ns.ContextPool.registerClient({ clientId: 0 }); + + return worker_ns; +} + +/** + * Create a buffered RIL worker. + * + * @return A worker object that stores sending octets in a internal buffer. + */ +function newUint8Worker() { + let worker = newWorker(); + let index = 0; // index for read + let buf = []; + + let context = worker.ContextPool._contexts[0]; + context.Buf.writeUint8 = function(value) { + buf.push(value); + }; + + context.Buf.readUint8 = function() { + return buf[index++]; + }; + + context.Buf.seekIncoming = function(offset) { + index += offset; + }; + + context.Buf.getReadAvailable = function() { + return buf.length - index; + }; + + worker.debug = do_print; + + return worker; +} + +/** + * Create a worker that keeps posted chrome message. + */ +function newInterceptWorker() { + let postedMessage; + let worker = newWorker({ + postRILMessage: function(data) { + }, + postMessage: function(message) { + postedMessage = message; + } + }); + return { + get postedMessage() { + return postedMessage; + }, + get worker() { + return worker; + } + }; +} + +/** + * Create a parcel suitable for postRILMessage(). + * + * @param fakeParcelSize + * Value to be written to parcel size field for testing + * incorrect/incomplete parcel reading. Replaced with correct + * one determined length of data if negative. + * @param response + * Response code of the incoming parcel. + * @param request + * Request code of the incoming parcel. + * @param data + * Extra data to be appended. + * + * @return an Uint8Array carrying all parcel data. + */ +function newIncomingParcel(fakeParcelSize, response, request, data) { + const UINT32_SIZE = 4; + const PARCEL_SIZE_SIZE = 4; + + let realParcelSize = data.length + 2 * UINT32_SIZE; + let buffer = new ArrayBuffer(realParcelSize + PARCEL_SIZE_SIZE); + let bytes = new Uint8Array(buffer); + + let writeIndex = 0; + function writeUint8(value) { + bytes[writeIndex] = value; + ++writeIndex; + } + + function writeInt32(value) { + writeUint8(value & 0xff); + writeUint8((value >> 8) & 0xff); + writeUint8((value >> 16) & 0xff); + writeUint8((value >> 24) & 0xff); + } + + function writeParcelSize(value) { + writeUint8((value >> 24) & 0xff); + writeUint8((value >> 16) & 0xff); + writeUint8((value >> 8) & 0xff); + writeUint8(value & 0xff); + } + + if (fakeParcelSize < 0) { + fakeParcelSize = realParcelSize; + } + writeParcelSize(fakeParcelSize); + + writeInt32(response); + writeInt32(request); + + // write parcel data + for (let ii = 0; ii < data.length; ++ii) { + writeUint8(data[ii]); + } + + return bytes; +} + +/** + * Create a parcel buffer which represents the hex string. + * + * @param hexString + * The HEX string to be converted. + * + * @return an Uint8Array carrying all parcel data. + */ +function hexStringToParcelByteArrayData(hexString) { + let length = Math.ceil((hexString.length / 2)); + let bytes = new Uint8Array(4 + length); + + bytes[0] = length & 0xFF; + bytes[1] = (length >> 8) & 0xFF; + bytes[2] = (length >> 16) & 0xFF; + bytes[3] = (length >> 24) & 0xFF; + + for (let i = 0; i < length; i ++) { + bytes[i + 4] = Number.parseInt(hexString.substr(i * 2, 2), 16); + } + + return bytes; +} diff --git a/dom/system/gonk/tests/marionette/head.js b/dom/system/gonk/tests/marionette/head.js new file mode 100644 index 000000000..5a6ee1272 --- /dev/null +++ b/dom/system/gonk/tests/marionette/head.js @@ -0,0 +1,345 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_CONTEXT = "chrome"; + +const SETTINGS_KEY_DATA_ENABLED = "ril.data.enabled"; +const SETTINGS_KEY_DATA_APN_SETTINGS = "ril.data.apnSettings"; +const SETTINGS_KEY_WIFI_ENABLED = "wifi.enabled"; + +const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed"; +const TOPIC_NETWORK_ACTIVE_CHANGED = "network-active-changed"; + +const NETWORK_STATE_UNKNOWN = Ci.nsINetworkInfo.NETWORK_STATE_UNKNOWN; +const NETWORK_STATE_CONNECTING = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTING; +const NETWORK_STATE_CONNECTED = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED; +const NETWORK_STATE_DISCONNECTING = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTING; +const NETWORK_STATE_DISCONNECTED = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED; + +const NETWORK_TYPE_MOBILE = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE; +const NETWORK_TYPE_MOBILE_MMS = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS; +const NETWORK_TYPE_MOBILE_SUPL = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_SUPL; +const NETWORK_TYPE_MOBILE_IMS = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_IMS; +const NETWORK_TYPE_MOBILE_DUN = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN; +const NETWORK_TYPE_MOBILE_FOTA = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_FOTA; + +const networkTypes = [ + NETWORK_TYPE_MOBILE, + NETWORK_TYPE_MOBILE_MMS, + NETWORK_TYPE_MOBILE_SUPL, + NETWORK_TYPE_MOBILE_IMS, + NETWORK_TYPE_MOBILE_DUN, + NETWORK_TYPE_MOBILE_FOTA +]; + +var Promise = Cu.import("resource://gre/modules/Promise.jsm").Promise; + +var ril = Cc["@mozilla.org/ril;1"].getService(Ci.nsIRadioInterfaceLayer); +ok(ril, "ril.constructor is " + ril.constructor); + +var radioInterface = ril.getRadioInterface(0); +ok(radioInterface, "radioInterface.constructor is " + radioInterface.constrctor); + +var _pendingEmulatorShellCmdCount = 0; +var _pendingEmulatorCmdCount = 0; + +/** + * Send emulator shell command with safe guard. + * + * We should only call |finish()| after all emulator shell command transactions + * end, so here comes with the pending counter. Resolve when the emulator + * shell gives response. Never reject. + * + * Fulfill params: + * result -- an array of emulator shell response lines. + * + * @param aCommands + * A string array commands to be passed to emulator through adb shell. + * + * @return A deferred promise. + */ +function runEmulatorShellCmdSafe(aCommands) { + return new Promise(function(aResolve, aReject) { + ++_pendingEmulatorShellCmdCount; + runEmulatorShell(aCommands, function(aResult) { + --_pendingEmulatorShellCmdCount; + + log("Emulator shell response: " + JSON.stringify(aResult)); + aResolve(aResult); + }); + }); +} + +/** + * Send emulator command with safe guard. + * + * We should only call |finish()| after all emulator command transactions + * end, so here comes with the pending counter. Resolve when the emulator + * gives positive response, and reject otherwise. + * + * Fulfill params: + * result -- an array of emulator response lines. + * Reject params: + * result -- an array of emulator response lines. + * + * @param aCommand + * A string command to be passed to emulator through its telnet console. + * + * @return A deferred promise. + */ +function runEmulatorCmdSafe(aCommand) { + log(aCommand); + return new Promise(function(aResolve, aReject) { + ++_pendingEmulatorCmdCount; + runEmulatorCmd(aCommand, function(aResult) { + --_pendingEmulatorCmdCount; + + log("Emulator console response: " + JSON.stringify(aResult)); + if (Array.isArray(aResult) && + aResult[aResult.length - 1] === "OK") { + aResolve(aResult); + } else { + aReject(aResult); + } + }); + }); +} + +/** + * Get mozSettings value specified by @aKey. + * + * Resolve if that mozSettings value is retrieved successfully, reject + * otherwise. + * + * Fulfill params: The corresponding mozSettings value of the key. + * Reject params: (none) + * + * @param aKey + * A string. + * @param aAllowError [optional] + * A boolean value. If set to true, an error response won't be treated + * as test failure. Default: false. + * + * @return A deferred promise. + */ +function getSettings(aKey, aAllowError) { + let request = window.navigator.mozSettings.createLock().get(aKey); + return request.then(function resolve(aValue) { + log("getSettings(" + aKey + ") - success"); + return aValue[aKey]; + }, function reject(aError) { + ok(aAllowError, "getSettings(" + aKey + ") - error"); + }); +} + +/** + * Set mozSettings values. + * + * Resolve if that mozSettings value is set successfully, reject otherwise. + * + * Fulfill params: (none) + * Reject params: (none) + * + * @param aKey + * A string key. + * @param aValue + * An object value. + * @param aAllowError [optional] + * A boolean value. If set to true, an error response won't be treated + * as test failure. Default: false. + * + * @return A deferred promise. + */ +function setSettings(aKey, aValue, aAllowError) { + let settings = {}; + settings[aKey] = aValue; + let lock = window.navigator.mozSettings.createLock(); + let request = lock.set(settings); + let deferred = Promise.defer(); + lock.onsettingstransactionsuccess = function () { + log("setSettings(" + JSON.stringify(settings) + ") - success"); + deferred.resolve(); + }; + lock.onsettingstransactionfailure = function () { + ok(aAllowError, "setSettings(" + JSON.stringify(settings) + ") - error"); + // We resolve even though we've thrown an error, since the ok() + // will do that. + deferred.resolve(); + }; + return deferred.promise; +} + +/** + * Wait for observer event. + * + * Resolve if that topic event occurs. Never reject. + * + * Fulfill params: the subject passed. + * + * @param aTopic + * A string topic name. + * + * @return A deferred promise. + */ +function waitForObserverEvent(aTopic) { + let obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); + let deferred = Promise.defer(); + + obs.addObserver(function observer(subject, topic, data) { + if (topic === aTopic) { + obs.removeObserver(observer, aTopic); + deferred.resolve(subject); + } + }, aTopic, false); + + return deferred.promise; +} + +/** + * Wait for one named event. + * + * Resolve if that named event occurs. Never reject. + * + * Fulfill params: the DOMEvent passed. + * + * @param aEventTarget + * An EventTarget object. + * @param aEventName + * A string event name. + * @param aMatchFun [optional] + * A matching function returns true or false to filter the event. + * + * @return A deferred promise. + */ +function waitForTargetEvent(aEventTarget, aEventName, aMatchFun) { + return new Promise(function(aResolve, aReject) { + aEventTarget.addEventListener(aEventName, function onevent(aEvent) { + if (!aMatchFun || aMatchFun(aEvent)) { + aEventTarget.removeEventListener(aEventName, onevent); + ok(true, "Event '" + aEventName + "' got."); + aResolve(aEvent); + } + }); + }); +} + +/** + * Set the default data connection enabling state, wait for + * "network-connection-state-changed" event and verify state. + * + * Fulfill params: instance of nsIRilNetworkInfo of the network connected. + * + * @param aEnabled + * A boolean state. + * + * @return A deferred promise. + */ +function setDataEnabledAndWait(aEnabled) { + let promises = []; + promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED) + .then(function(aSubject) { + ok(aSubject instanceof Ci.nsIRilNetworkInfo, + "subject should be an instance of nsIRilNetworkInfo"); + is(aSubject.type, NETWORK_TYPE_MOBILE, + "subject.type should be " + NETWORK_TYPE_MOBILE); + is(aSubject.state, + aEnabled ? Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED + : Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED, + "subject.state should be " + aEnabled ? "CONNECTED" : "DISCONNECTED"); + + return aSubject; + })); + promises.push(setSettings(SETTINGS_KEY_DATA_ENABLED, aEnabled)); + + return Promise.all(promises).then(aValues => aValues[0]); +} + +/** + * Setup a certain type of data connection, wait for + * "network-connection-state-changed" event and verify state. + * + * Fulfill params: instance of nsIRilNetworkInfo of the network connected. + * + * @param aNetworkType + * The mobile network type to setup. + * + * @return A deferred promise. + */ +function setupDataCallAndWait(aNetworkType) { + log("setupDataCallAndWait: " + aNetworkType); + + let promises = []; + promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED) + .then(function(aSubject) { + ok(aSubject instanceof Ci.nsIRilNetworkInfo, + "subject should be an instance of nsIRilNetworkInfo"); + is(aSubject.type, aNetworkType, + "subject.type should be " + aNetworkType); + is(aSubject.state, Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED, + "subject.state should be CONNECTED"); + + return aSubject; + })); + promises.push(radioInterface.setupDataCallByType(aNetworkType)); + + return Promise.all(promises).then(aValues => aValues[0]); +} + +/** + * Deactivate a certain type of data connection, wait for + * "network-connection-state-changed" event and verify state. + * + * Fulfill params: (none) + * + * @param aNetworkType + * The mobile network type to deactivate. + * + * @return A deferred promise. + */ +function deactivateDataCallAndWait(aNetworkType) { + log("deactivateDataCallAndWait: " + aNetworkType); + + let promises = []; + promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED) + .then(function(aSubject) { + ok(aSubject instanceof Ci.nsIRilNetworkInfo, + "subject should be an instance of nsIRilNetworkInfo"); + is(aSubject.type, aNetworkType, + "subject.type should be " + aNetworkType); + is(aSubject.state, Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED, + "subject.state should be DISCONNECTED"); + })); + promises.push(radioInterface.deactivateDataCallByType(aNetworkType)); + + return Promise.all(promises); +} + +/** + * Wait for pending emulator transactions and call |finish()|. + */ +function cleanUp() { + // Use ok here so that we have at least one test run. + ok(true, ":: CLEANING UP ::"); + + waitFor(finish, function() { + return _pendingEmulatorShellCmdCount === 0 && + _pendingEmulatorCmdCount === 0; + }); +} + +/** + * Basic test routine helper. + * + * This helper does nothing but clean-ups. + * + * @param aTestCaseMain + * A function that takes no parameter. + */ +function startTestBase(aTestCaseMain) { + Promise.resolve() + .then(aTestCaseMain) + .then(cleanUp, function(aException) { + ok(false, "promise rejects during test: " + aException); + cleanUp(); + }); +} diff --git a/dom/system/gonk/tests/marionette/manifest.ini b/dom/system/gonk/tests/marionette/manifest.ini new file mode 100644 index 000000000..528fe3baf --- /dev/null +++ b/dom/system/gonk/tests/marionette/manifest.ini @@ -0,0 +1,19 @@ +[DEFAULT] +run-if = buildapp == 'b2g' + +[test_geolocation.js] +skip-if = true # Bug 808783 +[test_fakevolume.js] +[test_ril_code_quality.py] +[test_screen_state.js] +[test_dsds_numRadioInterfaces.js] +[test_data_connection.js] +[test_network_active_changed.js] +[test_multiple_data_connection.js] +[test_data_connection_proxy.js] +[test_network_interface_list_service.js] +[test_all_network_info.js] +[test_network_interface_mtu.js] +skip-if = android_version < '19' +[test_timezone_changes.js] +skip-if = android_version < '19' diff --git a/dom/system/gonk/tests/marionette/ril_jshint/README.md b/dom/system/gonk/tests/marionette/ril_jshint/README.md new file mode 100644 index 000000000..a63967d63 --- /dev/null +++ b/dom/system/gonk/tests/marionette/ril_jshint/README.md @@ -0,0 +1,9 @@ +Test RIL Code Quality +===================== + +For more information, please refer to + +* Bug 880643 - B2G RIL: Add a code quality test on try server for RIL javascript code in gecko +* Slide: https://speakerdeck.com/aknow/improve-code-quality-of-ril-code-by-jshint +* Document: https://hackpad.com/Code-Quality-Test-For-RIL-Javascript-Code-In-Gecko-cz5j7YIGiw8 + diff --git a/dom/system/gonk/tests/marionette/ril_jshint/jshint.js b/dom/system/gonk/tests/marionette/ril_jshint/jshint.js new file mode 100644 index 000000000..ec5263a5b --- /dev/null +++ b/dom/system/gonk/tests/marionette/ril_jshint/jshint.js @@ -0,0 +1,11096 @@ +//2.1.3 +var JSHINT; +(function () { +var require; +require=(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; + +process.nextTick = (function () { + var canSetImmediate = typeof window !== 'undefined' + && window.setImmediate; + var canPost = typeof window !== 'undefined' + && window.postMessage && window.addEventListener + ; + + if (canSetImmediate) { + return function (f) { return window.setImmediate(f) }; + } + + if (canPost) { + var queue = []; + window.addEventListener('message', function (ev) { + if (ev.source === window && ev.data === 'process-tick') { + ev.stopPropagation(); + if (queue.length > 0) { + var fn = queue.shift(); + fn(); + } + } + }, true); + + return function nextTick(fn) { + queue.push(fn); + window.postMessage('process-tick', '*'); + }; + } + + return function nextTick(fn) { + setTimeout(fn, 0); + }; +})(); + +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +} + +// TODO(shtylman) +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; + +},{}],2:[function(require,module,exports){ +(function(process){if (!process.EventEmitter) process.EventEmitter = function () {}; + +var EventEmitter = exports.EventEmitter = process.EventEmitter; +var isArray = typeof Array.isArray === 'function' + ? Array.isArray + : function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]' + } +; +function indexOf (xs, x) { + if (xs.indexOf) return xs.indexOf(x); + for (var i = 0; i < xs.length; i++) { + if (x === xs[i]) return i; + } + return -1; +} + +// By default EventEmitters will print a warning if more than +// 10 listeners are added to it. This is a useful default which +// helps finding memory leaks. +// +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +var defaultMaxListeners = 10; +EventEmitter.prototype.setMaxListeners = function(n) { + if (!this._events) this._events = {}; + this._events.maxListeners = n; +}; + + +EventEmitter.prototype.emit = function(type) { + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events || !this._events.error || + (isArray(this._events.error) && !this._events.error.length)) + { + if (arguments[1] instanceof Error) { + throw arguments[1]; // Unhandled 'error' event + } else { + throw new Error("Uncaught, unspecified 'error' event."); + } + return false; + } + } + + if (!this._events) return false; + var handler = this._events[type]; + if (!handler) return false; + + if (typeof handler == 'function') { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + var args = Array.prototype.slice.call(arguments, 1); + handler.apply(this, args); + } + return true; + + } else if (isArray(handler)) { + var args = Array.prototype.slice.call(arguments, 1); + + var listeners = handler.slice(); + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + return true; + + } else { + return false; + } +}; + +// EventEmitter is defined in src/node_events.cc +// EventEmitter.prototype.emit() is also defined there. +EventEmitter.prototype.addListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('addListener only takes instances of Function'); + } + + if (!this._events) this._events = {}; + + // To avoid recursion in the case that type == "newListeners"! Before + // adding it to the listeners, first emit "newListeners". + this.emit('newListener', type, listener); + + if (!this._events[type]) { + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + } else if (isArray(this._events[type])) { + + // Check for listener leak + if (!this._events[type].warned) { + var m; + if (this._events.maxListeners !== undefined) { + m = this._events.maxListeners; + } else { + m = defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + console.trace(); + } + } + + // If we've already got an array, just append. + this._events[type].push(listener); + } else { + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + var self = this; + self.on(type, function g() { + self.removeListener(type, g); + listener.apply(this, arguments); + }); + + return this; +}; + +EventEmitter.prototype.removeListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('removeListener only takes instances of Function'); + } + + // does not use listeners(), so no side effect of creating _events[type] + if (!this._events || !this._events[type]) return this; + + var list = this._events[type]; + + if (isArray(list)) { + var i = indexOf(list, listener); + if (i < 0) return this; + list.splice(i, 1); + if (list.length == 0) + delete this._events[type]; + } else if (this._events[type] === listener) { + delete this._events[type]; + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + if (arguments.length === 0) { + this._events = {}; + return this; + } + + // does not use listeners(), so no side effect of creating _events[type] + if (type && this._events && this._events[type]) this._events[type] = null; + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + if (!this._events) this._events = {}; + if (!this._events[type]) this._events[type] = []; + if (!isArray(this._events[type])) { + this._events[type] = [this._events[type]]; + } + return this._events[type]; +}; + +})(require("__browserify_process")) +},{"__browserify_process":1}],3:[function(require,module,exports){ +(function(){// jshint -W001 + +"use strict"; + +// Identifiers provided by the ECMAScript standard. + +exports.reservedVars = { + arguments : false, + NaN : false +}; + +exports.ecmaIdentifiers = { + Array : false, + Boolean : false, + Date : false, + decodeURI : false, + decodeURIComponent : false, + encodeURI : false, + encodeURIComponent : false, + Error : false, + "eval" : false, + EvalError : false, + Function : false, + hasOwnProperty : false, + isFinite : false, + isNaN : false, + JSON : false, + Math : false, + Map : false, + Number : false, + Object : false, + parseInt : false, + parseFloat : false, + RangeError : false, + ReferenceError : false, + RegExp : false, + Set : false, + String : false, + SyntaxError : false, + TypeError : false, + URIError : false, + WeakMap : false +}; + +// Global variables commonly provided by a web browser environment. + +exports.browser = { + ArrayBuffer : false, + ArrayBufferView : false, + Audio : false, + Blob : false, + addEventListener : false, + applicationCache : false, + atob : false, + blur : false, + btoa : false, + clearInterval : false, + clearTimeout : false, + close : false, + closed : false, + DataView : false, + DOMParser : false, + defaultStatus : false, + document : false, + Element : false, + ElementTimeControl : false, + event : false, + FileReader : false, + Float32Array : false, + Float64Array : false, + FormData : false, + focus : false, + frames : false, + getComputedStyle : false, + HTMLElement : false, + HTMLAnchorElement : false, + HTMLBaseElement : false, + HTMLBlockquoteElement: false, + HTMLBodyElement : false, + HTMLBRElement : false, + HTMLButtonElement : false, + HTMLCanvasElement : false, + HTMLDirectoryElement : false, + HTMLDivElement : false, + HTMLDListElement : false, + HTMLFieldSetElement : false, + HTMLFontElement : false, + HTMLFormElement : false, + HTMLFrameElement : false, + HTMLFrameSetElement : false, + HTMLHeadElement : false, + HTMLHeadingElement : false, + HTMLHRElement : false, + HTMLHtmlElement : false, + HTMLIFrameElement : false, + HTMLImageElement : false, + HTMLInputElement : false, + HTMLIsIndexElement : false, + HTMLLabelElement : false, + HTMLLayerElement : false, + HTMLLegendElement : false, + HTMLLIElement : false, + HTMLLinkElement : false, + HTMLMapElement : false, + HTMLMenuElement : false, + HTMLMetaElement : false, + HTMLModElement : false, + HTMLObjectElement : false, + HTMLOListElement : false, + HTMLOptGroupElement : false, + HTMLOptionElement : false, + HTMLParagraphElement : false, + HTMLParamElement : false, + HTMLPreElement : false, + HTMLQuoteElement : false, + HTMLScriptElement : false, + HTMLSelectElement : false, + HTMLStyleElement : false, + HTMLTableCaptionElement: false, + HTMLTableCellElement : false, + HTMLTableColElement : false, + HTMLTableElement : false, + HTMLTableRowElement : false, + HTMLTableSectionElement: false, + HTMLTextAreaElement : false, + HTMLTitleElement : false, + HTMLUListElement : false, + HTMLVideoElement : false, + history : false, + Int16Array : false, + Int32Array : false, + Int8Array : false, + Image : false, + length : false, + localStorage : false, + location : false, + MessageChannel : false, + MessageEvent : false, + MessagePort : false, + moveBy : false, + moveTo : false, + MutationObserver : false, + name : false, + Node : false, + NodeFilter : false, + navigator : false, + onbeforeunload : true, + onblur : true, + onerror : true, + onfocus : true, + onload : true, + onresize : true, + onunload : true, + open : false, + openDatabase : false, + opener : false, + Option : false, + parent : false, + print : false, + removeEventListener : false, + resizeBy : false, + resizeTo : false, + screen : false, + scroll : false, + scrollBy : false, + scrollTo : false, + sessionStorage : false, + setInterval : false, + setTimeout : false, + SharedWorker : false, + status : false, + SVGAElement : false, + SVGAltGlyphDefElement: false, + SVGAltGlyphElement : false, + SVGAltGlyphItemElement: false, + SVGAngle : false, + SVGAnimateColorElement: false, + SVGAnimateElement : false, + SVGAnimateMotionElement: false, + SVGAnimateTransformElement: false, + SVGAnimatedAngle : false, + SVGAnimatedBoolean : false, + SVGAnimatedEnumeration: false, + SVGAnimatedInteger : false, + SVGAnimatedLength : false, + SVGAnimatedLengthList: false, + SVGAnimatedNumber : false, + SVGAnimatedNumberList: false, + SVGAnimatedPathData : false, + SVGAnimatedPoints : false, + SVGAnimatedPreserveAspectRatio: false, + SVGAnimatedRect : false, + SVGAnimatedString : false, + SVGAnimatedTransformList: false, + SVGAnimationElement : false, + SVGCSSRule : false, + SVGCircleElement : false, + SVGClipPathElement : false, + SVGColor : false, + SVGColorProfileElement: false, + SVGColorProfileRule : false, + SVGComponentTransferFunctionElement: false, + SVGCursorElement : false, + SVGDefsElement : false, + SVGDescElement : false, + SVGDocument : false, + SVGElement : false, + SVGElementInstance : false, + SVGElementInstanceList: false, + SVGEllipseElement : false, + SVGExternalResourcesRequired: false, + SVGFEBlendElement : false, + SVGFEColorMatrixElement: false, + SVGFEComponentTransferElement: false, + SVGFECompositeElement: false, + SVGFEConvolveMatrixElement: false, + SVGFEDiffuseLightingElement: false, + SVGFEDisplacementMapElement: false, + SVGFEDistantLightElement: false, + SVGFEDropShadowElement: false, + SVGFEFloodElement : false, + SVGFEFuncAElement : false, + SVGFEFuncBElement : false, + SVGFEFuncGElement : false, + SVGFEFuncRElement : false, + SVGFEGaussianBlurElement: false, + SVGFEImageElement : false, + SVGFEMergeElement : false, + SVGFEMergeNodeElement: false, + SVGFEMorphologyElement: false, + SVGFEOffsetElement : false, + SVGFEPointLightElement: false, + SVGFESpecularLightingElement: false, + SVGFESpotLightElement: false, + SVGFETileElement : false, + SVGFETurbulenceElement: false, + SVGFilterElement : false, + SVGFilterPrimitiveStandardAttributes: false, + SVGFitToViewBox : false, + SVGFontElement : false, + SVGFontFaceElement : false, + SVGFontFaceFormatElement: false, + SVGFontFaceNameElement: false, + SVGFontFaceSrcElement: false, + SVGFontFaceUriElement: false, + SVGForeignObjectElement: false, + SVGGElement : false, + SVGGlyphElement : false, + SVGGlyphRefElement : false, + SVGGradientElement : false, + SVGHKernElement : false, + SVGICCColor : false, + SVGImageElement : false, + SVGLangSpace : false, + SVGLength : false, + SVGLengthList : false, + SVGLineElement : false, + SVGLinearGradientElement: false, + SVGLocatable : false, + SVGMPathElement : false, + SVGMarkerElement : false, + SVGMaskElement : false, + SVGMatrix : false, + SVGMetadataElement : false, + SVGMissingGlyphElement: false, + SVGNumber : false, + SVGNumberList : false, + SVGPaint : false, + SVGPathElement : false, + SVGPathSeg : false, + SVGPathSegArcAbs : false, + SVGPathSegArcRel : false, + SVGPathSegClosePath : false, + SVGPathSegCurvetoCubicAbs: false, + SVGPathSegCurvetoCubicRel: false, + SVGPathSegCurvetoCubicSmoothAbs: false, + SVGPathSegCurvetoCubicSmoothRel: false, + SVGPathSegCurvetoQuadraticAbs: false, + SVGPathSegCurvetoQuadraticRel: false, + SVGPathSegCurvetoQuadraticSmoothAbs: false, + SVGPathSegCurvetoQuadraticSmoothRel: false, + SVGPathSegLinetoAbs : false, + SVGPathSegLinetoHorizontalAbs: false, + SVGPathSegLinetoHorizontalRel: false, + SVGPathSegLinetoRel : false, + SVGPathSegLinetoVerticalAbs: false, + SVGPathSegLinetoVerticalRel: false, + SVGPathSegList : false, + SVGPathSegMovetoAbs : false, + SVGPathSegMovetoRel : false, + SVGPatternElement : false, + SVGPoint : false, + SVGPointList : false, + SVGPolygonElement : false, + SVGPolylineElement : false, + SVGPreserveAspectRatio: false, + SVGRadialGradientElement: false, + SVGRect : false, + SVGRectElement : false, + SVGRenderingIntent : false, + SVGSVGElement : false, + SVGScriptElement : false, + SVGSetElement : false, + SVGStopElement : false, + SVGStringList : false, + SVGStylable : false, + SVGStyleElement : false, + SVGSwitchElement : false, + SVGSymbolElement : false, + SVGTRefElement : false, + SVGTSpanElement : false, + SVGTests : false, + SVGTextContentElement: false, + SVGTextElement : false, + SVGTextPathElement : false, + SVGTextPositioningElement: false, + SVGTitleElement : false, + SVGTransform : false, + SVGTransformList : false, + SVGTransformable : false, + SVGURIReference : false, + SVGUnitTypes : false, + SVGUseElement : false, + SVGVKernElement : false, + SVGViewElement : false, + SVGViewSpec : false, + SVGZoomAndPan : false, + TimeEvent : false, + top : false, + Uint16Array : false, + Uint32Array : false, + Uint8Array : false, + Uint8ClampedArray : false, + WebSocket : false, + window : false, + Worker : false, + XMLHttpRequest : false, + XMLSerializer : false, + XPathEvaluator : false, + XPathException : false, + XPathExpression : false, + XPathNSResolver : false, + XPathResult : false +}; + +exports.devel = { + alert : false, + confirm: false, + console: false, + Debug : false, + opera : false, + prompt : false +}; + +exports.worker = { + importScripts: true, + postMessage : true, + self : true +}; + +// Widely adopted global names that are not part of ECMAScript standard +exports.nonstandard = { + escape : false, + unescape: false +}; + +// Globals provided by popular JavaScript environments. + +exports.couch = { + "require" : false, + respond : false, + getRow : false, + emit : false, + send : false, + start : false, + sum : false, + log : false, + exports : false, + module : false, + provides : false +}; + +exports.node = { + __filename : false, + __dirname : false, + Buffer : false, + DataView : false, + console : false, + exports : true, // In Node it is ok to exports = module.exports = foo(); + GLOBAL : false, + global : false, + module : false, + process : false, + require : false, + setTimeout : false, + clearTimeout : false, + setInterval : false, + clearInterval : false, + setImmediate : false, // v0.9.1+ + clearImmediate: false // v0.9.1+ +}; + +exports.phantom = { + phantom : true, + require : true, + WebPage : true +}; + +exports.rhino = { + defineClass : false, + deserialize : false, + gc : false, + help : false, + importPackage: false, + "java" : false, + load : false, + loadClass : false, + print : false, + quit : false, + readFile : false, + readUrl : false, + runCommand : false, + seal : false, + serialize : false, + spawn : false, + sync : false, + toint32 : false, + version : false +}; + +exports.wsh = { + ActiveXObject : true, + Enumerator : true, + GetObject : true, + ScriptEngine : true, + ScriptEngineBuildVersion : true, + ScriptEngineMajorVersion : true, + ScriptEngineMinorVersion : true, + VBArray : true, + WSH : true, + WScript : true, + XDomainRequest : true +}; + +// Globals provided by popular JavaScript libraries. + +exports.dojo = { + dojo : false, + dijit : false, + dojox : false, + define : false, + "require": false +}; + +exports.jquery = { + "$" : false, + jQuery : false +}; + +exports.mootools = { + "$" : false, + "$$" : false, + Asset : false, + Browser : false, + Chain : false, + Class : false, + Color : false, + Cookie : false, + Core : false, + Document : false, + DomReady : false, + DOMEvent : false, + DOMReady : false, + Drag : false, + Element : false, + Elements : false, + Event : false, + Events : false, + Fx : false, + Group : false, + Hash : false, + HtmlTable : false, + Iframe : false, + IframeShim : false, + InputValidator: false, + instanceOf : false, + Keyboard : false, + Locale : false, + Mask : false, + MooTools : false, + Native : false, + Options : false, + OverText : false, + Request : false, + Scroller : false, + Slick : false, + Slider : false, + Sortables : false, + Spinner : false, + Swiff : false, + Tips : false, + Type : false, + typeOf : false, + URI : false, + Window : false +}; + +exports.prototypejs = { + "$" : false, + "$$" : false, + "$A" : false, + "$F" : false, + "$H" : false, + "$R" : false, + "$break" : false, + "$continue" : false, + "$w" : false, + Abstract : false, + Ajax : false, + Class : false, + Enumerable : false, + Element : false, + Event : false, + Field : false, + Form : false, + Hash : false, + Insertion : false, + ObjectRange : false, + PeriodicalExecuter: false, + Position : false, + Prototype : false, + Selector : false, + Template : false, + Toggle : false, + Try : false, + Autocompleter : false, + Builder : false, + Control : false, + Draggable : false, + Draggables : false, + Droppables : false, + Effect : false, + Sortable : false, + SortableObserver : false, + Sound : false, + Scriptaculous : false +}; + +exports.yui = { + YUI : false, + Y : false, + YUI_config: false +}; + + +})() +},{}],4:[function(require,module,exports){ +"use strict"; + +var state = { + syntax: {}, + + reset: function () { + this.tokens = { + prev: null, + next: null, + curr: null + }; + + this.option = {}; + this.ignored = {}; + this.directive = {}; + this.jsonMode = false; + this.jsonWarnings = []; + this.lines = []; + this.tab = ""; + this.cache = {}; // Node.JS doesn't have Map. Sniff. + } +}; + +exports.state = state; + +},{}],5:[function(require,module,exports){ +(function(){"use strict"; + +exports.register = function (linter) { + // Check for properties named __proto__. This special property was + // deprecated and then re-introduced for ES6. + + linter.on("Identifier", function style_scanProto(data) { + if (linter.getOption("proto")) { + return; + } + + if (data.name === "__proto__") { + linter.warn("W103", { + line: data.line, + char: data.char, + data: [ data.name ] + }); + } + }); + + // Check for properties named __iterator__. This is a special property + // available only in browsers with JavaScript 1.7 implementation. + + linter.on("Identifier", function style_scanIterator(data) { + if (linter.getOption("iterator")) { + return; + } + + if (data.name === "__iterator__") { + linter.warn("W104", { + line: data.line, + char: data.char, + data: [ data.name ] + }); + } + }); + + // Check for dangling underscores. + + linter.on("Identifier", function style_scanDangling(data) { + if (!linter.getOption("nomen")) { + return; + } + + // Underscore.js + if (data.name === "_") { + return; + } + + // In Node, __dirname and __filename should be ignored. + if (linter.getOption("node")) { + if (/^(__dirname|__filename)$/.test(data.name) && !data.isProperty) { + return; + } + } + + if (/^(_+.*|.*_+)$/.test(data.name)) { + linter.warn("W105", { + line: data.line, + char: data.from, + data: [ "dangling '_'", data.name ] + }); + } + }); + + // Check that all identifiers are using camelCase notation. + // Exceptions: names like MY_VAR and _myVar. + + linter.on("Identifier", function style_scanCamelCase(data) { + if (!linter.getOption("camelcase")) { + return; + } + + if (data.name.replace(/^_+/, "").indexOf("_") > -1 && !data.name.match(/^[A-Z0-9_]*$/)) { + linter.warn("W106", { + line: data.line, + char: data.from, + data: [ data.name ] + }); + } + }); + + // Enforce consistency in style of quoting. + + linter.on("String", function style_scanQuotes(data) { + var quotmark = linter.getOption("quotmark"); + var code; + + if (!quotmark) { + return; + } + + // If quotmark is set to 'single' warn about all double-quotes. + + if (quotmark === "single" && data.quote !== "'") { + code = "W109"; + } + + // If quotmark is set to 'double' warn about all single-quotes. + + if (quotmark === "double" && data.quote !== "\"") { + code = "W108"; + } + + // If quotmark is set to true, remember the first quotation style + // and then warn about all others. + + if (quotmark === true) { + if (!linter.getCache("quotmark")) { + linter.setCache("quotmark", data.quote); + } + + if (linter.getCache("quotmark") !== data.quote) { + code = "W110"; + } + } + + if (code) { + linter.warn(code, { + line: data.line, + char: data.char, + }); + } + }); + + linter.on("Number", function style_scanNumbers(data) { + if (data.value.charAt(0) === ".") { + // Warn about a leading decimal point. + linter.warn("W008", { + line: data.line, + char: data.char, + data: [ data.value ] + }); + } + + if (data.value.substr(data.value.length - 1) === ".") { + // Warn about a trailing decimal point. + linter.warn("W047", { + line: data.line, + char: data.char, + data: [ data.value ] + }); + } + + if (/^00+/.test(data.value)) { + // Multiple leading zeroes. + linter.warn("W046", { + line: data.line, + char: data.char, + data: [ data.value ] + }); + } + }); + + // Warn about script URLs. + + linter.on("String", function style_scanJavaScriptURLs(data) { + var re = /^(?:javascript|jscript|ecmascript|vbscript|mocha|livescript)\s*:/i; + + if (linter.getOption("scripturl")) { + return; + } + + if (re.test(data.value)) { + linter.warn("W107", { + line: data.line, + char: data.char + }); + } + }); +}; +})() +},{}],6:[function(require,module,exports){ +/* + * Regular expressions. Some of these are stupidly long. + */ + +/*jshint maxlen:1000 */ + +"use string"; + +// Unsafe comment or string (ax) +exports.unsafeString = + /@cc|<\/?|script|\]\s*\]|<\s*!|</i; + +// Unsafe characters that are silently deleted by one or more browsers (cx) +exports.unsafeChars = + /[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/; + +// Characters in strings that need escaping (nx and nxg) +exports.needEsc = + /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/; + +exports.needEscGlobal = + /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + +// Star slash (lx) +exports.starSlash = /\*\//; + +// Identifier (ix) +exports.identifier = /^([a-zA-Z_$][a-zA-Z0-9_$]*)$/; + +// JavaScript URL (jx) +exports.javascriptURL = /^(?:javascript|jscript|ecmascript|vbscript|mocha|livescript)\s*:/i; + +// Catches /* falls through */ comments (ft) +//exports.fallsThrough = /^\s*\/\*\s*falls?\sthrough\s*\*\/\s*$/; +exports.fallsThrough = /^\s*\/\/\s*Falls?\sthrough.*\s*$/; + +},{}],7:[function(require,module,exports){ +(function(global){/*global window, global*/ +var util = require("util") +var assert = require("assert") + +var slice = Array.prototype.slice +var console +var times = {} + +if (typeof global !== "undefined" && global.console) { + console = global.console +} else if (typeof window !== "undefined" && window.console) { + console = window.console +} else { + console = window.console = {} +} + +var functions = [ + [log, "log"] + , [info, "info"] + , [warn, "warn"] + , [error, "error"] + , [time, "time"] + , [timeEnd, "timeEnd"] + , [trace, "trace"] + , [dir, "dir"] + , [assert, "assert"] +] + +for (var i = 0; i < functions.length; i++) { + var tuple = functions[i] + var f = tuple[0] + var name = tuple[1] + + if (!console[name]) { + console[name] = f + } +} + +module.exports = console + +function log() {} + +function info() { + console.log.apply(console, arguments) +} + +function warn() { + console.log.apply(console, arguments) +} + +function error() { + console.warn.apply(console, arguments) +} + +function time(label) { + times[label] = Date.now() +} + +function timeEnd(label) { + var time = times[label] + if (!time) { + throw new Error("No such label: " + label) + } + + var duration = Date.now() - time + console.log(label + ": " + duration + "ms") +} + +function trace() { + var err = new Error() + err.name = "Trace" + err.message = util.format.apply(null, arguments) + console.error(err.stack) +} + +function dir(object) { + console.log(util.inspect(object) + "\n") +} + +function assert(expression) { + if (!expression) { + var arr = slice.call(arguments, 1) + assert.ok(false, util.format.apply(null, arr)) + } +} + +})(window) +},{"util":8,"assert":9}],10:[function(require,module,exports){ +(function(){/* + * Lexical analysis and token construction. + */ + +"use strict"; + +var _ = require("underscore"); +var events = require("events"); +var reg = require("./reg.js"); +var state = require("./state.js").state; + +// Some of these token types are from JavaScript Parser API +// while others are specific to JSHint parser. +// JS Parser API: https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API + +var Token = { + Identifier: 1, + Punctuator: 2, + NumericLiteral: 3, + StringLiteral: 4, + Comment: 5, + Keyword: 6, + NullLiteral: 7, + BooleanLiteral: 8, + RegExp: 9 +}; + +// This is auto generated from the unicode tables. +// The tables are at: +// http://www.fileformat.info/info/unicode/category/Lu/list.htm +// http://www.fileformat.info/info/unicode/category/Ll/list.htm +// http://www.fileformat.info/info/unicode/category/Lt/list.htm +// http://www.fileformat.info/info/unicode/category/Lm/list.htm +// http://www.fileformat.info/info/unicode/category/Lo/list.htm +// http://www.fileformat.info/info/unicode/category/Nl/list.htm + +var unicodeLetterTable = [ + 170, 170, 181, 181, 186, 186, 192, 214, + 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, + 880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908, + 910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366, + 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610, + 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, + 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, + 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069, + 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361, + 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431, + 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, + 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, + 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, + 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, + 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, + 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785, + 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, + 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, + 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, + 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, + 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123, + 3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212, + 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, + 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, + 3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455, + 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, + 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, + 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, + 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, + 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805, + 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138, + 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198, + 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346, + 4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696, + 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, + 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, + 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740, + 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, + 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, + 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312, + 6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516, + 6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823, + 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141, + 7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409, + 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, + 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, + 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, + 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, + 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455, + 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486, + 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521, + 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, + 11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621, + 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, + 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, + 11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295, + 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, + 12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589, + 12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312, + 19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124, + 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539, + 42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783, + 42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921, + 43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042, + 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259, + 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442, + 43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595, + 43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697, + 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714, + 43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798, + 43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032, + 55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045, + 64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279, + 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, + 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, + 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, + 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370, + 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, + 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, + 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, + 65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334, + 66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511, + 66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592, + 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, + 67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115, + 68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405, + 68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687, + 69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894, + 92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964, + 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, + 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, + 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, + 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, + 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, + 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, + 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, + 131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972, + 177984, 177984, 178205, 178205, 194560, 195101 +]; + +var identifierStartTable = []; + +for (var i = 0; i < 128; i++) { + identifierStartTable[i] = + i === 36 || // $ + i >= 65 && i <= 90 || // A-Z + i === 95 || // _ + i >= 97 && i <= 122; // a-z +} + +var identifierPartTable = []; + +for (var i = 0; i < 128; i++) { + identifierPartTable[i] = + identifierStartTable[i] || // $, _, A-Z, a-z + i >= 48 && i <= 57; // 0-9 +} + +// Object that handles postponed lexing verifications that checks the parsed +// environment state. + +function asyncTrigger() { + var _checks = []; + + return { + push: function (fn) { + _checks.push(fn); + }, + + check: function () { + for (var check in _checks) { + _checks[check](); + } + + _checks.splice(0, _checks.length); + } + }; +} + +/* + * Lexer for JSHint. + * + * This object does a char-by-char scan of the provided source code + * and produces a sequence of tokens. + * + * var lex = new Lexer("var i = 0;"); + * lex.start(); + * lex.token(); // returns the next token + * + * You have to use the token() method to move the lexer forward + * but you don't have to use its return value to get tokens. In addition + * to token() method returning the next token, the Lexer object also + * emits events. + * + * lex.on("Identifier", function (data) { + * if (data.name.indexOf("_") >= 0) { + * // Produce a warning. + * } + * }); + * + * Note that the token() method returns tokens in a JSLint-compatible + * format while the event emitter uses a slightly modified version of + * Mozilla's JavaScript Parser API. Eventually, we will move away from + * JSLint format. + */ +function Lexer(source) { + var lines = source; + + if (typeof lines === "string") { + lines = lines + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n") + .split("\n"); + } + + // If the first line is a shebang (#!), make it a blank and move on. + // Shebangs are used by Node scripts. + + if (lines[0] && lines[0].substr(0, 2) === "#!") { + lines[0] = ""; + } + + this.emitter = new events.EventEmitter(); + this.source = source; + this.lines = lines; + this.prereg = true; + + this.line = 0; + this.char = 1; + this.from = 1; + this.input = ""; + + for (var i = 0; i < state.option.indent; i += 1) { + state.tab += " "; + } +} + +Lexer.prototype = { + _lines: [], + + get lines() { + this._lines = state.lines; + return this._lines; + }, + + set lines(val) { + this._lines = val; + state.lines = this._lines; + }, + + /* + * Return the next i character without actually moving the + * char pointer. + */ + peek: function (i) { + return this.input.charAt(i || 0); + }, + + /* + * Move the char pointer forward i times. + */ + skip: function (i) { + i = i || 1; + this.char += i; + this.input = this.input.slice(i); + }, + + /* + * Subscribe to a token event. The API for this method is similar + * Underscore.js i.e. you can subscribe to multiple events with + * one call: + * + * lex.on("Identifier Number", function (data) { + * // ... + * }); + */ + on: function (names, listener) { + names.split(" ").forEach(function (name) { + this.emitter.on(name, listener); + }.bind(this)); + }, + + /* + * Trigger a token event. All arguments will be passed to each + * listener. + */ + trigger: function () { + this.emitter.emit.apply(this.emitter, Array.prototype.slice.call(arguments)); + }, + + /* + * Postpone a token event. the checking condition is set as + * last parameter, and the trigger function is called in a + * stored callback. To be later called using the check() function + * by the parser. This avoids parser's peek() to give the lexer + * a false context. + */ + triggerAsync: function (type, args, checks, fn) { + checks.push(function () { + if (fn()) { + this.trigger(type, args); + } + }.bind(this)); + }, + + /* + * Extract a punctuator out of the next sequence of characters + * or return 'null' if its not possible. + * + * This method's implementation was heavily influenced by the + * scanPunctuator function in the Esprima parser's source code. + */ + scanPunctuator: function () { + var ch1 = this.peek(); + var ch2, ch3, ch4; + + switch (ch1) { + // Most common single-character punctuators + case ".": + if ((/^[0-9]$/).test(this.peek(1))) { + return null; + } + if (this.peek(1) === "." && this.peek(2) === ".") { + return { + type: Token.Punctuator, + value: "..." + }; + } + /* falls through */ + case "(": + case ")": + case ";": + case ",": + case "{": + case "}": + case "[": + case "]": + case ":": + case "~": + case "?": + return { + type: Token.Punctuator, + value: ch1 + }; + + // A pound sign (for Node shebangs) + case "#": + return { + type: Token.Punctuator, + value: ch1 + }; + + // We're at the end of input + case "": + return null; + } + + // Peek more characters + + ch2 = this.peek(1); + ch3 = this.peek(2); + ch4 = this.peek(3); + + // 4-character punctuator: >>>= + + if (ch1 === ">" && ch2 === ">" && ch3 === ">" && ch4 === "=") { + return { + type: Token.Punctuator, + value: ">>>=" + }; + } + + // 3-character punctuators: === !== >>> <<= >>= + + if (ch1 === "=" && ch2 === "=" && ch3 === "=") { + return { + type: Token.Punctuator, + value: "===" + }; + } + + if (ch1 === "!" && ch2 === "=" && ch3 === "=") { + return { + type: Token.Punctuator, + value: "!==" + }; + } + + if (ch1 === ">" && ch2 === ">" && ch3 === ">") { + return { + type: Token.Punctuator, + value: ">>>" + }; + } + + if (ch1 === "<" && ch2 === "<" && ch3 === "=") { + return { + type: Token.Punctuator, + value: "<<=" + }; + } + + if (ch1 === ">" && ch2 === ">" && ch3 === "=") { + return { + type: Token.Punctuator, + value: ">>=" + }; + } + + // Fat arrow punctuator + if (ch1 === "=" && ch2 === ">") { + return { + type: Token.Punctuator, + value: ch1 + ch2 + }; + } + + // 2-character punctuators: <= >= == != ++ -- << >> && || + // += -= *= %= &= |= ^= (but not /=, see below) + if (ch1 === ch2 && ("+-<>&|".indexOf(ch1) >= 0)) { + return { + type: Token.Punctuator, + value: ch1 + ch2 + }; + } + + if ("<>=!+-*%&|^".indexOf(ch1) >= 0) { + if (ch2 === "=") { + return { + type: Token.Punctuator, + value: ch1 + ch2 + }; + } + + return { + type: Token.Punctuator, + value: ch1 + }; + } + + // Special case: /=. We need to make sure that this is an + // operator and not a regular expression. + + if (ch1 === "/") { + if (ch2 === "=" && /\/=(?!(\S*\/[gim]?))/.test(this.input)) { + // /= is not a part of a regular expression, return it as a + // punctuator. + return { + type: Token.Punctuator, + value: "/=" + }; + } + + return { + type: Token.Punctuator, + value: "/" + }; + } + + return null; + }, + + /* + * Extract a comment out of the next sequence of characters and/or + * lines or return 'null' if its not possible. Since comments can + * span across multiple lines this method has to move the char + * pointer. + * + * In addition to normal JavaScript comments (// and /*) this method + * also recognizes JSHint- and JSLint-specific comments such as + * /*jshint, /*jslint, /*globals and so on. + */ + scanComments: function () { + var ch1 = this.peek(); + var ch2 = this.peek(1); + var rest = this.input.substr(2); + var startLine = this.line; + var startChar = this.char; + + // Create a comment token object and make sure it + // has all the data JSHint needs to work with special + // comments. + + function commentToken(label, body, opt) { + var special = ["jshint", "jslint", "members", "member", "globals", "global", "exported"]; + var isSpecial = false; + var value = label + body; + var commentType = "plain"; + opt = opt || {}; + + if (opt.isMultiline) { + value += "*/"; + } + + special.forEach(function (str) { + if (isSpecial) { + return; + } + + // Don't recognize any special comments other than jshint for single-line + // comments. This introduced many problems with legit comments. + if (label === "//" && str !== "jshint") { + return; + } + + if (body.substr(0, str.length) === str) { + isSpecial = true; + label = label + str; + body = body.substr(str.length); + } + + if (!isSpecial && body.charAt(0) === " " && body.substr(1, str.length) === str) { + isSpecial = true; + label = label + " " + str; + body = body.substr(str.length + 1); + } + + if (!isSpecial) { + return; + } + + switch (str) { + case "member": + commentType = "members"; + break; + case "global": + commentType = "globals"; + break; + default: + commentType = str; + } + }); + + return { + type: Token.Comment, + commentType: commentType, + value: value, + body: body, + isSpecial: isSpecial, + isMultiline: opt.isMultiline || false, + isMalformed: opt.isMalformed || false + }; + } + + // End of unbegun comment. Raise an error and skip that input. + if (ch1 === "*" && ch2 === "/") { + this.trigger("error", { + code: "E018", + line: startLine, + character: startChar + }); + + this.skip(2); + return null; + } + + // Comments must start either with // or /* + if (ch1 !== "/" || (ch2 !== "*" && ch2 !== "/")) { + return null; + } + + // One-line comment + if (ch2 === "/") { + this.skip(this.input.length); // Skip to the EOL. + return commentToken("//", rest); + } + + var body = ""; + + /* Multi-line comment */ + if (ch2 === "*") { + this.skip(2); + + while (this.peek() !== "*" || this.peek(1) !== "/") { + if (this.peek() === "") { // End of Line + body += "\n"; + + // If we hit EOF and our comment is still unclosed, + // trigger an error and end the comment implicitly. + if (!this.nextLine()) { + this.trigger("error", { + code: "E017", + line: startLine, + character: startChar + }); + + return commentToken("/*", body, { + isMultiline: true, + isMalformed: true + }); + } + } else { + body += this.peek(); + this.skip(); + } + } + + this.skip(2); + return commentToken("/*", body, { isMultiline: true }); + } + }, + + /* + * Extract a keyword out of the next sequence of characters or + * return 'null' if its not possible. + */ + scanKeyword: function () { + var result = /^[a-zA-Z_$][a-zA-Z0-9_$]*/.exec(this.input); + var keywords = [ + "if", "in", "do", "var", "for", "new", + "try", "let", "this", "else", "case", + "void", "with", "enum", "while", "break", + "catch", "throw", "const", "yield", "class", + "super", "return", "typeof", "delete", + "switch", "export", "import", "default", + "finally", "extends", "function", "continue", + "debugger", "instanceof" + ]; + + if (result && keywords.indexOf(result[0]) >= 0) { + return { + type: Token.Keyword, + value: result[0] + }; + } + + return null; + }, + + /* + * Extract a JavaScript identifier out of the next sequence of + * characters or return 'null' if its not possible. In addition, + * to Identifier this method can also produce BooleanLiteral + * (true/false) and NullLiteral (null). + */ + scanIdentifier: function () { + var id = ""; + var index = 0; + var type, char; + + // Detects any character in the Unicode categories "Uppercase + // letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter + // (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or + // "Letter number (Nl)". + // + // Both approach and unicodeLetterTable were borrowed from + // Google's Traceur. + + function isUnicodeLetter(code) { + for (var i = 0; i < unicodeLetterTable.length;) { + if (code < unicodeLetterTable[i++]) { + return false; + } + + if (code <= unicodeLetterTable[i++]) { + return true; + } + } + + return false; + } + + function isHexDigit(str) { + return (/^[0-9a-fA-F]$/).test(str); + } + + var readUnicodeEscapeSequence = function () { + /*jshint validthis:true */ + index += 1; + + if (this.peek(index) !== "u") { + return null; + } + + var ch1 = this.peek(index + 1); + var ch2 = this.peek(index + 2); + var ch3 = this.peek(index + 3); + var ch4 = this.peek(index + 4); + var code; + + if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) { + code = parseInt(ch1 + ch2 + ch3 + ch4, 16); + + if (isUnicodeLetter(code)) { + index += 5; + return "\\u" + ch1 + ch2 + ch3 + ch4; + } + + return null; + } + + return null; + }.bind(this); + + var getIdentifierStart = function () { + /*jshint validthis:true */ + var chr = this.peek(index); + var code = chr.charCodeAt(0); + + if (code === 92) { + return readUnicodeEscapeSequence(); + } + + if (code < 128) { + if (identifierStartTable[code]) { + index += 1; + return chr; + } + + return null; + } + + if (isUnicodeLetter(code)) { + index += 1; + return chr; + } + + return null; + }.bind(this); + + var getIdentifierPart = function () { + /*jshint validthis:true */ + var chr = this.peek(index); + var code = chr.charCodeAt(0); + + if (code === 92) { + return readUnicodeEscapeSequence(); + } + + if (code < 128) { + if (identifierPartTable[code]) { + index += 1; + return chr; + } + + return null; + } + + if (isUnicodeLetter(code)) { + index += 1; + return chr; + } + + return null; + }.bind(this); + + char = getIdentifierStart(); + if (char === null) { + return null; + } + + id = char; + for (;;) { + char = getIdentifierPart(); + + if (char === null) { + break; + } + + id += char; + } + + switch (id) { + case "true": + case "false": + type = Token.BooleanLiteral; + break; + case "null": + type = Token.NullLiteral; + break; + default: + type = Token.Identifier; + } + + return { + type: type, + value: id + }; + }, + + /* + * Extract a numeric literal out of the next sequence of + * characters or return 'null' if its not possible. This method + * supports all numeric literals described in section 7.8.3 + * of the EcmaScript 5 specification. + * + * This method's implementation was heavily influenced by the + * scanNumericLiteral function in the Esprima parser's source code. + */ + scanNumericLiteral: function () { + var index = 0; + var value = ""; + var length = this.input.length; + var char = this.peek(index); + var bad; + + function isDecimalDigit(str) { + return (/^[0-9]$/).test(str); + } + + function isOctalDigit(str) { + return (/^[0-7]$/).test(str); + } + + function isHexDigit(str) { + return (/^[0-9a-fA-F]$/).test(str); + } + + function isIdentifierStart(ch) { + return (ch === "$") || (ch === "_") || (ch === "\\") || + (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z"); + } + + // Numbers must start either with a decimal digit or a point. + + if (char !== "." && !isDecimalDigit(char)) { + return null; + } + + if (char !== ".") { + value = this.peek(index); + index += 1; + char = this.peek(index); + + if (value === "0") { + // Base-16 numbers. + if (char === "x" || char === "X") { + index += 1; + value += char; + + while (index < length) { + char = this.peek(index); + if (!isHexDigit(char)) { + break; + } + value += char; + index += 1; + } + + if (value.length <= 2) { // 0x + return { + type: Token.NumericLiteral, + value: value, + isMalformed: true + }; + } + + if (index < length) { + char = this.peek(index); + if (isIdentifierStart(char)) { + return null; + } + } + + return { + type: Token.NumericLiteral, + value: value, + base: 16, + isMalformed: false + }; + } + + // Base-8 numbers. + if (isOctalDigit(char)) { + index += 1; + value += char; + bad = false; + + while (index < length) { + char = this.peek(index); + + // Numbers like '019' (note the 9) are not valid octals + // but we still parse them and mark as malformed. + + if (isDecimalDigit(char)) { + bad = true; + } else if (!isOctalDigit(char)) { + break; + } + value += char; + index += 1; + } + + if (index < length) { + char = this.peek(index); + if (isIdentifierStart(char)) { + return null; + } + } + + return { + type: Token.NumericLiteral, + value: value, + base: 8, + isMalformed: false + }; + } + + // Decimal numbers that start with '0' such as '09' are illegal + // but we still parse them and return as malformed. + + if (isDecimalDigit(char)) { + index += 1; + value += char; + } + } + + while (index < length) { + char = this.peek(index); + if (!isDecimalDigit(char)) { + break; + } + value += char; + index += 1; + } + } + + // Decimal digits. + + if (char === ".") { + value += char; + index += 1; + + while (index < length) { + char = this.peek(index); + if (!isDecimalDigit(char)) { + break; + } + value += char; + index += 1; + } + } + + // Exponent part. + + if (char === "e" || char === "E") { + value += char; + index += 1; + char = this.peek(index); + + if (char === "+" || char === "-") { + value += this.peek(index); + index += 1; + } + + char = this.peek(index); + if (isDecimalDigit(char)) { + value += char; + index += 1; + + while (index < length) { + char = this.peek(index); + if (!isDecimalDigit(char)) { + break; + } + value += char; + index += 1; + } + } else { + return null; + } + } + + if (index < length) { + char = this.peek(index); + if (isIdentifierStart(char)) { + return null; + } + } + + return { + type: Token.NumericLiteral, + value: value, + base: 10, + isMalformed: !isFinite(value) + }; + }, + + /* + * Extract a string out of the next sequence of characters and/or + * lines or return 'null' if its not possible. Since strings can + * span across multiple lines this method has to move the char + * pointer. + * + * This method recognizes pseudo-multiline JavaScript strings: + * + * var str = "hello\ + * world"; + */ + scanStringLiteral: function (checks) { + /*jshint loopfunc:true */ + var quote = this.peek(); + + // String must start with a quote. + if (quote !== "\"" && quote !== "'") { + return null; + } + + // In JSON strings must always use double quotes. + this.triggerAsync("warning", { + code: "W108", + line: this.line, + character: this.char // +1? + }, checks, function () { return state.jsonMode && quote !== "\""; }); + + var value = ""; + var startLine = this.line; + var startChar = this.char; + var allowNewLine = false; + + this.skip(); + + while (this.peek() !== quote) { + while (this.peek() === "") { // End Of Line + + // If an EOL is not preceded by a backslash, show a warning + // and proceed like it was a legit multi-line string where + // author simply forgot to escape the newline symbol. + // + // Another approach is to implicitly close a string on EOL + // but it generates too many false positives. + + if (!allowNewLine) { + this.trigger("warning", { + code: "W112", + line: this.line, + character: this.char + }); + } else { + allowNewLine = false; + + // Otherwise show a warning if multistr option was not set. + // For JSON, show warning no matter what. + + this.triggerAsync("warning", { + code: "W043", + line: this.line, + character: this.char + }, checks, function () { return !state.option.multistr; }); + + this.triggerAsync("warning", { + code: "W042", + line: this.line, + character: this.char + }, checks, function () { return state.jsonMode && state.option.multistr; }); + } + + // If we get an EOF inside of an unclosed string, show an + // error and implicitly close it at the EOF point. + + if (!this.nextLine()) { + this.trigger("error", { + code: "E029", + line: startLine, + character: startChar + }); + + return { + type: Token.StringLiteral, + value: value, + isUnclosed: true, + quote: quote + }; + } + } + + allowNewLine = false; + var char = this.peek(); + var jump = 1; // A length of a jump, after we're done + // parsing this character. + + if (char < " ") { + // Warn about a control character in a string. + this.trigger("warning", { + code: "W113", + line: this.line, + character: this.char, + data: [ "<non-printable>" ] + }); + } + + // Special treatment for some escaped characters. + + if (char === "\\") { + this.skip(); + char = this.peek(); + + switch (char) { + case "'": + this.triggerAsync("warning", { + code: "W114", + line: this.line, + character: this.char, + data: [ "\\'" ] + }, checks, function () {return state.jsonMode; }); + break; + case "b": + char = "\b"; + break; + case "f": + char = "\f"; + break; + case "n": + char = "\n"; + break; + case "r": + char = "\r"; + break; + case "t": + char = "\t"; + break; + case "0": + char = "\0"; + + // Octal literals fail in strict mode. + // Check if the number is between 00 and 07. + var n = parseInt(this.peek(1), 10); + this.triggerAsync("warning", { + code: "W115", + line: this.line, + character: this.char + }, checks, + function () { return n >= 0 && n <= 7 && state.directive["use strict"]; }); + break; + case "u": + char = String.fromCharCode(parseInt(this.input.substr(1, 4), 16)); + jump = 5; + break; + case "v": + this.triggerAsync("warning", { + code: "W114", + line: this.line, + character: this.char, + data: [ "\\v" ] + }, checks, function () { return state.jsonMode; }); + + char = "\v"; + break; + case "x": + var x = parseInt(this.input.substr(1, 2), 16); + + this.triggerAsync("warning", { + code: "W114", + line: this.line, + character: this.char, + data: [ "\\x-" ] + }, checks, function () { return state.jsonMode; }); + + char = String.fromCharCode(x); + jump = 3; + break; + case "\\": + case "\"": + case "/": + break; + case "": + allowNewLine = true; + char = ""; + break; + case "!": + if (value.slice(value.length - 2) === "<") { + break; + } + + /*falls through */ + default: + // Weird escaping. + this.trigger("warning", { + code: "W044", + line: this.line, + character: this.char + }); + } + } + + value += char; + this.skip(jump); + } + + this.skip(); + return { + type: Token.StringLiteral, + value: value, + isUnclosed: false, + quote: quote + }; + }, + + /* + * Extract a regular expression out of the next sequence of + * characters and/or lines or return 'null' if its not possible. + * + * This method is platform dependent: it accepts almost any + * regular expression values but then tries to compile and run + * them using system's RegExp object. This means that there are + * rare edge cases where one JavaScript engine complains about + * your regular expression while others don't. + */ + scanRegExp: function () { + var index = 0; + var length = this.input.length; + var char = this.peek(); + var value = char; + var body = ""; + var flags = []; + var malformed = false; + var isCharSet = false; + var terminated; + + var scanUnexpectedChars = function () { + // Unexpected control character + if (char < " ") { + malformed = true; + this.trigger("warning", { + code: "W048", + line: this.line, + character: this.char + }); + } + + // Unexpected escaped character + if (char === "<") { + malformed = true; + this.trigger("warning", { + code: "W049", + line: this.line, + character: this.char, + data: [ char ] + }); + } + }.bind(this); + + // Regular expressions must start with '/' + if (!this.prereg || char !== "/") { + return null; + } + + index += 1; + terminated = false; + + // Try to get everything in between slashes. A couple of + // cases aside (see scanUnexpectedChars) we don't really + // care whether the resulting expression is valid or not. + // We will check that later using the RegExp object. + + while (index < length) { + char = this.peek(index); + value += char; + body += char; + + if (isCharSet) { + if (char === "]") { + if (this.peek(index - 1) !== "\\" || this.peek(index - 2) === "\\") { + isCharSet = false; + } + } + + if (char === "\\") { + index += 1; + char = this.peek(index); + body += char; + value += char; + + scanUnexpectedChars(); + } + + index += 1; + continue; + } + + if (char === "\\") { + index += 1; + char = this.peek(index); + body += char; + value += char; + + scanUnexpectedChars(); + + if (char === "/") { + index += 1; + continue; + } + + if (char === "[") { + index += 1; + continue; + } + } + + if (char === "[") { + isCharSet = true; + index += 1; + continue; + } + + if (char === "/") { + body = body.substr(0, body.length - 1); + terminated = true; + index += 1; + break; + } + + index += 1; + } + + // A regular expression that was never closed is an + // error from which we cannot recover. + + if (!terminated) { + this.trigger("error", { + code: "E015", + line: this.line, + character: this.from + }); + + return void this.trigger("fatal", { + line: this.line, + from: this.from + }); + } + + // Parse flags (if any). + + while (index < length) { + char = this.peek(index); + if (!/[gim]/.test(char)) { + break; + } + flags.push(char); + value += char; + index += 1; + } + + // Check regular expression for correctness. + + try { + new RegExp(body, flags.join("")); + } catch (err) { + malformed = true; + this.trigger("error", { + code: "E016", + line: this.line, + character: this.char, + data: [ err.message ] // Platform dependent! + }); + } + + return { + type: Token.RegExp, + value: value, + flags: flags, + isMalformed: malformed + }; + }, + + /* + * Scan for any occurence of mixed tabs and spaces. If smarttabs option + * is on, ignore tabs followed by spaces. + * + * Tabs followed by one space followed by a block comment are allowed. + */ + scanMixedSpacesAndTabs: function () { + var at, match; + + if (state.option.smarttabs) { + // Negative look-behind for "//" + match = this.input.match(/(\/\/|^\s?\*)? \t/); + at = match && !match[1] ? 0 : -1; + } else { + at = this.input.search(/ \t|\t [^\*]/); + } + + return at; + }, + + /* + * Scan for characters that get silently deleted by one or more browsers. + */ + scanUnsafeChars: function () { + return this.input.search(reg.unsafeChars); + }, + + /* + * Produce the next raw token or return 'null' if no tokens can be matched. + * This method skips over all space characters. + */ + next: function (checks) { + this.from = this.char; + + // Move to the next non-space character. + var start; + if (/\s/.test(this.peek())) { + start = this.char; + + while (/\s/.test(this.peek())) { + this.from += 1; + this.skip(); + } + + if (this.peek() === "") { // EOL + if (!/^\s*$/.test(this.lines[this.line - 1]) && state.option.trailing) { + this.trigger("warning", { code: "W102", line: this.line, character: start }); + } + } + } + + // Methods that work with multi-line structures and move the + // character pointer. + + var match = this.scanComments() || + this.scanStringLiteral(checks); + + if (match) { + return match; + } + + // Methods that don't move the character pointer. + + match = + this.scanRegExp() || + this.scanPunctuator() || + this.scanKeyword() || + this.scanIdentifier() || + this.scanNumericLiteral(); + + if (match) { + this.skip(match.value.length); + return match; + } + + // No token could be matched, give up. + + return null; + }, + + /* + * Switch to the next line and reset all char pointers. Once + * switched, this method also checks for mixed spaces and tabs + * and other minor warnings. + */ + nextLine: function () { + var char; + + if (this.line >= this.lines.length) { + return false; + } + + this.input = this.lines[this.line]; + this.line += 1; + this.char = 1; + this.from = 1; + + char = this.scanMixedSpacesAndTabs(); + if (char >= 0) { + this.trigger("warning", { code: "W099", line: this.line, character: char + 1 }); + } + + this.input = this.input.replace(/\t/g, state.tab); + char = this.scanUnsafeChars(); + + if (char >= 0) { + this.trigger("warning", { code: "W100", line: this.line, character: char }); + } + + // If there is a limit on line length, warn when lines get too + // long. + + if (state.option.maxlen && state.option.maxlen < this.input.length) { + this.trigger("warning", { code: "W101", line: this.line, character: this.input.length }); + } + + return true; + }, + + /* + * This is simply a synonym for nextLine() method with a friendlier + * public name. + */ + start: function () { + this.nextLine(); + }, + + /* + * Produce the next token. This function is called by advance() to get + * the next token. It retuns a token in a JSLint-compatible format. + */ + token: function () { + /*jshint loopfunc:true */ + var checks = asyncTrigger(); + var token; + + + function isReserved(token, isProperty) { + if (!token.reserved) { + return false; + } + + if (token.meta && token.meta.isFutureReservedWord) { + // ES3 FutureReservedWord in an ES5 environment. + if (state.option.inES5(true) && !token.meta.es5) { + return false; + } + + // Some ES5 FutureReservedWord identifiers are active only + // within a strict mode environment. + if (token.meta.strictOnly) { + if (!state.option.strict && !state.directive["use strict"]) { + return false; + } + } + + if (isProperty) { + return false; + } + } + + return true; + } + + // Produce a token object. + var create = function (type, value, isProperty) { + /*jshint validthis:true */ + var obj; + + if (type !== "(endline)" && type !== "(end)") { + this.prereg = false; + } + + if (type === "(punctuator)") { + switch (value) { + case ".": + case ")": + case "~": + case "#": + case "]": + this.prereg = false; + break; + default: + this.prereg = true; + } + + obj = Object.create(state.syntax[value] || state.syntax["(error)"]); + } + + if (type === "(identifier)") { + if (value === "return" || value === "case" || value === "typeof") { + this.prereg = true; + } + + if (_.has(state.syntax, value)) { + obj = Object.create(state.syntax[value] || state.syntax["(error)"]); + + // If this can't be a reserved keyword, reset the object. + if (!isReserved(obj, isProperty && type === "(identifier)")) { + obj = null; + } + } + } + + if (!obj) { + obj = Object.create(state.syntax[type]); + } + + obj.identifier = (type === "(identifier)"); + obj.type = obj.type || type; + obj.value = value; + obj.line = this.line; + obj.character = this.char; + obj.from = this.from; + + if (isProperty && obj.identifier) { + obj.isProperty = isProperty; + } + + obj.check = checks.check; + + return obj; + }.bind(this); + + for (;;) { + if (!this.input.length) { + return create(this.nextLine() ? "(endline)" : "(end)", ""); + } + + token = this.next(checks); + + if (!token) { + if (this.input.length) { + // Unexpected character. + this.trigger("error", { + code: "E024", + line: this.line, + character: this.char, + data: [ this.peek() ] + }); + + this.input = ""; + } + + continue; + } + + switch (token.type) { + case Token.StringLiteral: + this.triggerAsync("String", { + line: this.line, + char: this.char, + from: this.from, + value: token.value, + quote: token.quote + }, checks, function () { return true; }); + + return create("(string)", token.value); + case Token.Identifier: + this.trigger("Identifier", { + line: this.line, + char: this.char, + from: this.form, + name: token.value, + isProperty: state.tokens.curr.id === "." + }); + + /* falls through */ + case Token.Keyword: + case Token.NullLiteral: + case Token.BooleanLiteral: + return create("(identifier)", token.value, state.tokens.curr.id === "."); + + case Token.NumericLiteral: + if (token.isMalformed) { + this.trigger("warning", { + code: "W045", + line: this.line, + character: this.char, + data: [ token.value ] + }); + } + + this.triggerAsync("warning", { + code: "W114", + line: this.line, + character: this.char, + data: [ "0x-" ] + }, checks, function () { return token.base === 16 && state.jsonMode; }); + + this.triggerAsync("warning", { + code: "W115", + line: this.line, + character: this.char + }, checks, function () { + return state.directive["use strict"] && token.base === 8; + }); + + this.trigger("Number", { + line: this.line, + char: this.char, + from: this.from, + value: token.value, + base: token.base, + isMalformed: token.malformed + }); + + return create("(number)", token.value); + + case Token.RegExp: + return create("(regexp)", token.value); + + case Token.Comment: + state.tokens.curr.comment = true; + + if (token.isSpecial) { + return { + value: token.value, + body: token.body, + type: token.commentType, + isSpecial: token.isSpecial, + line: this.line, + character: this.char, + from: this.from + }; + } + + break; + + case "": + break; + + default: + return create("(punctuator)", token.value); + } + } + } +}; + +exports.Lexer = Lexer; + +})() +},{"events":2,"./reg.js":6,"./state.js":4,"underscore":11}],"jshint":[function(require,module,exports){ +module.exports=require('E/GbHF'); +},{}],"E/GbHF":[function(require,module,exports){ +(function(){/*! + * JSHint, by JSHint Community. + * + * This file (and this file only) is licensed under the same slightly modified + * MIT license that JSLint is. It stops evil-doers everywhere: + * + * Copyright (c) 2002 Douglas Crockford (www.JSLint.com) + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom + * the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * The Software shall be used for Good, not Evil. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jshint quotmark:double */ +/*global console:true */ +/*exported console */ + +var _ = require("underscore"); +var events = require("events"); +var vars = require("../shared/vars.js"); +var messages = require("../shared/messages.js"); +var Lexer = require("./lex.js").Lexer; +var reg = require("./reg.js"); +var state = require("./state.js").state; +var style = require("./style.js"); + +// We need this module here because environments such as IE and Rhino +// don't necessarilly expose the 'console' API and browserify uses +// it to log things. It's a sad state of affair, really. +var console = require("console-browserify"); + +// We build the application inside a function so that we produce only a singleton +// variable. That function will be invoked immediately, and its return value is +// the JSHINT function itself. + +var JSHINT = (function () { + "use strict"; + + var anonname, // The guessed name for anonymous functions. + api, // Extension API + + // These are operators that should not be used with the ! operator. + bang = { + "<" : true, + "<=" : true, + "==" : true, + "===": true, + "!==": true, + "!=" : true, + ">" : true, + ">=" : true, + "+" : true, + "-" : true, + "*" : true, + "/" : true, + "%" : true + }, + + // These are the JSHint boolean options. + boolOptions = { + asi : true, // if automatic semicolon insertion should be tolerated + bitwise : true, // if bitwise operators should not be allowed + boss : true, // if advanced usage of assignments should be allowed + browser : true, // if the standard browser globals should be predefined + camelcase : true, // if identifiers should be required in camel case + couch : true, // if CouchDB globals should be predefined + curly : true, // if curly braces around all blocks should be required + debug : true, // if debugger statements should be allowed + devel : true, // if logging globals should be predefined (console, alert, etc.) + dojo : true, // if Dojo Toolkit globals should be predefined + eqeqeq : true, // if === should be required + eqnull : true, // if == null comparisons should be tolerated + es3 : true, // if ES3 syntax should be allowed + es5 : true, // if ES5 syntax should be allowed (is now set per default) + esnext : true, // if es.next specific syntax should be allowed + moz : true, // if mozilla specific syntax should be allowed + evil : true, // if eval should be allowed + expr : true, // if ExpressionStatement should be allowed as Programs + forin : true, // if for in statements must filter + funcscope : true, // if only function scope should be used for scope tests + gcl : true, // if JSHint should be compatible with Google Closure Linter + globalstrict: true, // if global "use strict"; should be allowed (also enables 'strict') + immed : true, // if immediate invocations must be wrapped in parens + iterator : true, // if the `__iterator__` property should be allowed + jquery : true, // if jQuery globals should be predefined + lastsemic : true, // if semicolons may be ommitted for the trailing + // statements inside of a one-line blocks. + laxbreak : true, // if line breaks should not be checked + laxcomma : true, // if line breaks should not be checked around commas + loopfunc : true, // if functions should be allowed to be defined within + // loops + mootools : true, // if MooTools globals should be predefined + multistr : true, // allow multiline strings + newcap : true, // if constructor names must be capitalized + noarg : true, // if arguments.caller and arguments.callee should be + // disallowed + node : true, // if the Node.js environment globals should be + // predefined + noempty : true, // if empty blocks should be disallowed + nonew : true, // if using `new` for side-effects should be disallowed + nonstandard : true, // if non-standard (but widely adopted) globals should + // be predefined + nomen : true, // if names should be checked + onevar : true, // if only one var statement per function should be + // allowed + passfail : true, // if the scan should stop on first error + phantom : true, // if PhantomJS symbols should be allowed + plusplus : true, // if increment/decrement should not be allowed + proto : true, // if the `__proto__` property should be allowed + prototypejs : true, // if Prototype and Scriptaculous globals should be + // predefined + rhino : true, // if the Rhino environment globals should be predefined + undef : true, // if variables should be declared before used + scripturl : true, // if script-targeted URLs should be tolerated + shadow : true, // if variable shadowing should be tolerated + smarttabs : true, // if smarttabs should be tolerated + // (http://www.emacswiki.org/emacs/SmartTabs) + strict : true, // require the "use strict"; pragma + sub : true, // if all forms of subscript notation are tolerated + supernew : true, // if `new function () { ... };` and `new Object;` + // should be tolerated + trailing : true, // if trailing whitespace rules apply + validthis : true, // if 'this' inside a non-constructor function is valid. + // This is a function scoped option only. + withstmt : true, // if with statements should be allowed + white : true, // if strict whitespace rules apply + worker : true, // if Web Worker script symbols should be allowed + wsh : true, // if the Windows Scripting Host environment globals + // should be predefined + yui : true, // YUI variables should be predefined + + // Obsolete options + onecase : true, // if one case switch statements should be allowed + regexp : true, // if the . should not be allowed in regexp literals + regexdash : true // if unescaped first/last dash (-) inside brackets + // should be tolerated + }, + + // These are the JSHint options that can take any value + // (we use this object to detect invalid options) + valOptions = { + maxlen : false, + indent : false, + maxerr : false, + predef : false, + quotmark : false, //'single'|'double'|true + scope : false, + maxstatements: false, // {int} max statements per function + maxdepth : false, // {int} max nested block depth per function + maxparams : false, // {int} max params per function + maxcomplexity: false, // {int} max cyclomatic complexity per function + unused : true, // warn if variables are unused. Available options: + // false - don't check for unused variables + // true - "vars" + check last function param + // "vars" - skip checking unused function params + // "strict" - "vars" + check all function params + latedef : false // warn if the variable is used before its definition + // false - don't emit any warnings + // true - warn if any variable is used before its definition + // "nofunc" - warn for any variable but function declarations + }, + + // These are JSHint boolean options which are shared with JSLint + // where the definition in JSHint is opposite JSLint + invertedOptions = { + bitwise : true, + forin : true, + newcap : true, + nomen : true, + plusplus: true, + regexp : true, + undef : true, + white : true, + + // Inverted and renamed, use JSHint name here + eqeqeq : true, + onevar : true, + strict : true + }, + + // These are JSHint boolean options which are shared with JSLint + // where the name has been changed but the effect is unchanged + renamedOptions = { + eqeq : "eqeqeq", + vars : "onevar", + windows: "wsh", + sloppy : "strict" + }, + + declared, // Globals that were declared using /*global ... */ syntax. + exported, // Variables that are used outside of the current file. + + functionicity = [ + "closure", "exception", "global", "label", + "outer", "unused", "var" + ], + + funct, // The current function + functions, // All of the functions + + global, // The global scope + implied, // Implied globals + inblock, + indent, + lookahead, + lex, + member, + membersOnly, + noreach, + predefined, // Global variables defined by option + + scope, // The current scope + stack, + unuseds, + urls, + warnings, + + extraModules = [], + emitter = new events.EventEmitter(); + + function checkOption(name, t) { + name = name.trim(); + + if (/^[+-]W\d{3}$/g.test(name)) { + return true; + } + + if (valOptions[name] === undefined && boolOptions[name] === undefined) { + if (t.type !== "jslint") { + error("E001", t, name); + return false; + } + } + + return true; + } + + function isString(obj) { + return Object.prototype.toString.call(obj) === "[object String]"; + } + + function isIdentifier(tkn, value) { + if (!tkn) + return false; + + if (!tkn.identifier || tkn.value !== value) + return false; + + return true; + } + + function isReserved(token) { + if (!token.reserved) { + return false; + } + + if (token.meta && token.meta.isFutureReservedWord) { + // ES3 FutureReservedWord in an ES5 environment. + if (state.option.inES5(true) && !token.meta.es5) { + return false; + } + + // Some ES5 FutureReservedWord identifiers are active only + // within a strict mode environment. + if (token.meta.strictOnly) { + if (!state.option.strict && !state.directive["use strict"]) { + return false; + } + } + + if (token.isProperty) { + return false; + } + } + + return true; + } + + function supplant(str, data) { + return str.replace(/\{([^{}]*)\}/g, function (a, b) { + var r = data[b]; + return typeof r === "string" || typeof r === "number" ? r : a; + }); + } + + function combine(t, o) { + var n; + for (n in o) { + if (_.has(o, n) && !_.has(JSHINT.blacklist, n)) { + t[n] = o[n]; + } + } + } + + function updatePredefined() { + Object.keys(JSHINT.blacklist).forEach(function (key) { + delete predefined[key]; + }); + } + + function assume() { + if (state.option.es5) { + warning("I003"); + } + if (state.option.couch) { + combine(predefined, vars.couch); + } + + if (state.option.rhino) { + combine(predefined, vars.rhino); + } + + if (state.option.phantom) { + combine(predefined, vars.phantom); + } + + if (state.option.prototypejs) { + combine(predefined, vars.prototypejs); + } + + if (state.option.node) { + combine(predefined, vars.node); + } + + if (state.option.devel) { + combine(predefined, vars.devel); + } + + if (state.option.dojo) { + combine(predefined, vars.dojo); + } + + if (state.option.browser) { + combine(predefined, vars.browser); + } + + if (state.option.nonstandard) { + combine(predefined, vars.nonstandard); + } + + if (state.option.jquery) { + combine(predefined, vars.jquery); + } + + if (state.option.mootools) { + combine(predefined, vars.mootools); + } + + if (state.option.worker) { + combine(predefined, vars.worker); + } + + if (state.option.wsh) { + combine(predefined, vars.wsh); + } + + if (state.option.globalstrict && state.option.strict !== false) { + state.option.strict = true; + } + + if (state.option.yui) { + combine(predefined, vars.yui); + } + + // Let's assume that chronologically ES3 < ES5 < ES6/ESNext < Moz + + state.option.inMoz = function (strict) { + if (strict) { + return state.option.moz && !state.option.esnext; + } + return state.option.moz; + }; + + state.option.inESNext = function (strict) { + if (strict) { + return !state.option.moz && state.option.esnext; + } + return state.option.moz || state.option.esnext; + }; + + state.option.inES5 = function (/* strict */) { + return !state.option.es3; + }; + + state.option.inES3 = function (strict) { + if (strict) { + return !state.option.moz && !state.option.esnext && state.option.es3; + } + return state.option.es3; + }; + } + + // Produce an error warning. + function quit(code, line, chr) { + var percentage = Math.floor((line / state.lines.length) * 100); + var message = messages.errors[code].desc; + + throw { + name: "JSHintError", + line: line, + character: chr, + message: message + " (" + percentage + "% scanned).", + raw: message + }; + } + + function isundef(scope, code, token, a) { + return JSHINT.undefs.push([scope, code, token, a]); + } + + function warning(code, t, a, b, c, d) { + var ch, l, w, msg; + + if (/^W\d{3}$/.test(code)) { + if (state.ignored[code]) + return; + + msg = messages.warnings[code]; + } else if (/E\d{3}/.test(code)) { + msg = messages.errors[code]; + } else if (/I\d{3}/.test(code)) { + msg = messages.info[code]; + } + + t = t || state.tokens.next; + if (t.id === "(end)") { // `~ + t = state.tokens.curr; + } + + l = t.line || 0; + ch = t.from || 0; + + w = { + id: "(error)", + raw: msg.desc, + code: msg.code, + evidence: state.lines[l - 1] || "", + line: l, + character: ch, + scope: JSHINT.scope, + a: a, + b: b, + c: c, + d: d + }; + + w.reason = supplant(msg.desc, w); + JSHINT.errors.push(w); + + if (state.option.passfail) { + quit("E042", l, ch); + } + + warnings += 1; + if (warnings >= state.option.maxerr) { + quit("E043", l, ch); + } + + return w; + } + + function warningAt(m, l, ch, a, b, c, d) { + return warning(m, { + line: l, + from: ch + }, a, b, c, d); + } + + function error(m, t, a, b, c, d) { + warning(m, t, a, b, c, d); + } + + function errorAt(m, l, ch, a, b, c, d) { + return error(m, { + line: l, + from: ch + }, a, b, c, d); + } + + // Tracking of "internal" scripts, like eval containing a static string + function addInternalSrc(elem, src) { + var i; + i = { + id: "(internal)", + elem: elem, + value: src + }; + JSHINT.internals.push(i); + return i; + } + + function addlabel(t, type, tkn, islet) { + // Define t in the current function in the current scope. + if (type === "exception") { + if (_.has(funct["(context)"], t)) { + if (funct[t] !== true && !state.option.node) { + warning("W002", state.tokens.next, t); + } + } + } + + if (_.has(funct, t) && !funct["(global)"]) { + if (funct[t] === true) { + if (state.option.latedef) { + if ((state.option.latedef === true && _.contains([funct[t], type], "unction")) || + !_.contains([funct[t], type], "unction")) { + warning("W003", state.tokens.next, t); + } + } + } else { + if (!state.option.shadow && type !== "exception" || + (funct["(blockscope)"].getlabel(t))) { + warning("W004", state.tokens.next, t); + } + } + } + + // a double definition of a let variable in same block throws a TypeError + //if (funct["(blockscope)"] && funct["(blockscope)"].current.has(t)) { + // error("E044", state.tokens.next, t); + //} + + // if the identifier is from a let, adds it only to the current blockscope + if (islet) { + funct["(blockscope)"].current.add(t, type, state.tokens.curr); + } else { + + funct[t] = type; + + if (tkn) { + funct["(tokens)"][t] = tkn; + } + + if (funct["(global)"]) { + global[t] = funct; + if (_.has(implied, t)) { + if (state.option.latedef) { + if ((state.option.latedef === true && _.contains([funct[t], type], "unction")) || + !_.contains([funct[t], type], "unction")) { + warning("W003", state.tokens.next, t); + } + } + + delete implied[t]; + } + } else { + scope[t] = funct; + } + } + } + + function doOption() { + var nt = state.tokens.next; + var body = nt.body.split(",").map(function (s) { return s.trim(); }); + var predef = {}; + + if (nt.type === "globals") { + body.forEach(function (g) { + g = g.split(":"); + var key = g[0]; + var val = g[1]; + + if (key.charAt(0) === "-") { + key = key.slice(1); + val = false; + + JSHINT.blacklist[key] = key; + updatePredefined(); + } else { + predef[key] = (val === "true"); + } + }); + + combine(predefined, predef); + + for (var key in predef) { + if (_.has(predef, key)) { + declared[key] = nt; + } + } + } + + if (nt.type === "exported") { + body.forEach(function (e) { + exported[e] = true; + }); + } + + if (nt.type === "members") { + membersOnly = membersOnly || {}; + + body.forEach(function (m) { + var ch1 = m.charAt(0); + var ch2 = m.charAt(m.length - 1); + + if (ch1 === ch2 && (ch1 === "\"" || ch1 === "'")) { + m = m + .substr(1, m.length - 2) + .replace("\\b", "\b") + .replace("\\t", "\t") + .replace("\\n", "\n") + .replace("\\v", "\v") + .replace("\\f", "\f") + .replace("\\r", "\r") + .replace("\\\\", "\\") + .replace("\\\"", "\""); + } + + membersOnly[m] = false; + }); + } + + var numvals = [ + "maxstatements", + "maxparams", + "maxdepth", + "maxcomplexity", + "maxerr", + "maxlen", + "indent" + ]; + + if (nt.type === "jshint" || nt.type === "jslint") { + body.forEach(function (g) { + g = g.split(":"); + var key = (g[0] || "").trim(); + var val = (g[1] || "").trim(); + + if (!checkOption(key, nt)) { + return; + } + + if (numvals.indexOf(key) >= 0) { + + // GH988 - numeric options can be disabled by setting them to `false` + if (val !== "false") { + val = +val; + + if (typeof val !== "number" || !isFinite(val) || val <= 0 || Math.floor(val) !== val) { + error("E032", nt, g[1].trim()); + return; + } + + if (key === "indent") { + state.option["(explicitIndent)"] = true; + } + state.option[key] = val; + } else { + if (key === "indent") { + state.option["(explicitIndent)"] = false; + } else { + state.option[key] = false; + } + } + + return; + } + + if (key === "validthis") { + // `validthis` is valid only within a function scope. + if (funct["(global)"]) { + error("E009"); + } else { + if (val === "true" || val === "false") { + state.option.validthis = (val === "true"); + } else { + error("E002", nt); + } + } + return; + } + + if (key === "quotmark") { + switch (val) { + case "true": + case "false": + state.option.quotmark = (val === "true"); + break; + case "double": + case "single": + state.option.quotmark = val; + break; + default: + error("E002", nt); + } + return; + } + + if (key === "unused") { + switch (val) { + case "true": + state.option.unused = true; + break; + case "false": + state.option.unused = false; + break; + case "vars": + case "strict": + state.option.unused = val; + break; + default: + error("E002", nt); + } + return; + } + + if (key === "latedef") { + switch (val) { + case "true": + state.option.latedef = true; + break; + case "false": + state.option.latedef = false; + break; + case "nofunc": + state.option.latedef = "nofunc"; + break; + default: + error("E002", nt); + } + return; + } + + var match = /^([+-])(W\d{3})$/g.exec(key); + if (match) { + // ignore for -W..., unignore for +W... + state.ignored[match[2]] = (match[1] === "-"); + return; + } + + var tn; + if (val === "true" || val === "false") { + if (nt.type === "jslint") { + tn = renamedOptions[key] || key; + state.option[tn] = (val === "true"); + + if (invertedOptions[tn] !== undefined) { + state.option[tn] = !state.option[tn]; + } + } else { + state.option[key] = (val === "true"); + } + + if (key === "newcap") { + state.option["(explicitNewcap)"] = true; + } + return; + } + + error("E002", nt); + }); + + assume(); + } + } + + // We need a peek function. If it has an argument, it peeks that much farther + // ahead. It is used to distinguish + // for ( var i in ... + // from + // for ( var i = ... + + function peek(p) { + var i = p || 0, j = 0, t; + + while (j <= i) { + t = lookahead[j]; + if (!t) { + t = lookahead[j] = lex.token(); + } + j += 1; + } + return t; + } + + // Produce the next token. It looks for programming errors. + + function advance(id, t) { + switch (state.tokens.curr.id) { + case "(number)": + if (state.tokens.next.id === ".") { + warning("W005", state.tokens.curr); + } + break; + case "-": + if (state.tokens.next.id === "-" || state.tokens.next.id === "--") { + warning("W006"); + } + break; + case "+": + if (state.tokens.next.id === "+" || state.tokens.next.id === "++") { + warning("W007"); + } + break; + } + + if (state.tokens.curr.type === "(string)" || state.tokens.curr.identifier) { + anonname = state.tokens.curr.value; + } + + if (id && state.tokens.next.id !== id) { + if (t) { + if (state.tokens.next.id === "(end)") { + error("E019", t, t.id); + } else { + error("E020", state.tokens.next, id, t.id, t.line, state.tokens.next.value); + } + } else if (state.tokens.next.type !== "(identifier)" || state.tokens.next.value !== id) { + warning("W116", state.tokens.next, id, state.tokens.next.value); + } + } + + state.tokens.prev = state.tokens.curr; + state.tokens.curr = state.tokens.next; + for (;;) { + state.tokens.next = lookahead.shift() || lex.token(); + + if (!state.tokens.next) { // No more tokens left, give up + quit("E041", state.tokens.curr.line); + } + + if (state.tokens.next.id === "(end)" || state.tokens.next.id === "(error)") { + return; + } + + if (state.tokens.next.check) { + state.tokens.next.check(); + } + + if (state.tokens.next.isSpecial) { + doOption(); + } else { + if (state.tokens.next.id !== "(endline)") { + break; + } + } + } + } + + // This is the heart of JSHINT, the Pratt parser. In addition to parsing, it + // is looking for ad hoc lint patterns. We add .fud to Pratt's model, which is + // like .nud except that it is only used on the first token of a statement. + // Having .fud makes it much easier to define statement-oriented languages like + // JavaScript. I retained Pratt's nomenclature. + + // .nud Null denotation + // .fud First null denotation + // .led Left denotation + // lbp Left binding power + // rbp Right binding power + + // They are elements of the parsing method called Top Down Operator Precedence. + + function expression(rbp, initial) { + var left, isArray = false, isObject = false, isLetExpr = false; + + // if current expression is a let expression + if (!initial && state.tokens.next.value === "let" && peek(0).value === "(") { + if (!state.option.inMoz(true)) { + warning("W118", state.tokens.next, "let expressions"); + } + isLetExpr = true; + // create a new block scope we use only for the current expression + funct["(blockscope)"].stack(); + advance("let"); + advance("("); + state.syntax["let"].fud.call(state.syntax["let"].fud, false); + advance(")"); + } + + if (state.tokens.next.id === "(end)") + error("E006", state.tokens.curr); + + advance(); + + if (initial) { + anonname = "anonymous"; + funct["(verb)"] = state.tokens.curr.value; + } + + if (initial === true && state.tokens.curr.fud) { + left = state.tokens.curr.fud(); + } else { + if (state.tokens.curr.nud) { + left = state.tokens.curr.nud(); + } else { + error("E030", state.tokens.curr, state.tokens.curr.id); + } + + var end_of_expr = state.tokens.next.identifier && + !state.tokens.curr.led && + state.tokens.curr.line !== state.tokens.next.line; + while (rbp < state.tokens.next.lbp && !end_of_expr) { + isArray = state.tokens.curr.value === "Array"; + isObject = state.tokens.curr.value === "Object"; + + // #527, new Foo.Array(), Foo.Array(), new Foo.Object(), Foo.Object() + // Line breaks in IfStatement heads exist to satisfy the checkJSHint + // "Line too long." error. + if (left && (left.value || (left.first && left.first.value))) { + // If the left.value is not "new", or the left.first.value is a "." + // then safely assume that this is not "new Array()" and possibly + // not "new Object()"... + if (left.value !== "new" || + (left.first && left.first.value && left.first.value === ".")) { + isArray = false; + // ...In the case of Object, if the left.value and state.tokens.curr.value + // are not equal, then safely assume that this not "new Object()" + if (left.value !== state.tokens.curr.value) { + isObject = false; + } + } + } + + advance(); + + if (isArray && state.tokens.curr.id === "(" && state.tokens.next.id === ")") { + warning("W009", state.tokens.curr); + } + + if (isObject && state.tokens.curr.id === "(" && state.tokens.next.id === ")") { + warning("W010", state.tokens.curr); + } + + if (left && state.tokens.curr.led) { + left = state.tokens.curr.led(left); + } else { + error("E033", state.tokens.curr, state.tokens.curr.id); + } + } + } + if (isLetExpr) { + funct["(blockscope)"].unstack(); + } + return left; + } + + +// Functions for conformance of style. + + function adjacent(left, right) { + left = left || state.tokens.curr; + right = right || state.tokens.next; + if (state.option.white) { + if (left.character !== right.from && left.line === right.line) { + left.from += (left.character - left.from); + warning("W011", left, left.value); + } + } + } + + function nobreak(left, right) { + left = left || state.tokens.curr; + right = right || state.tokens.next; + if (state.option.white && (left.character !== right.from || left.line !== right.line)) { + warning("W012", right, right.value); + } + } + + function nospace(left, right) { + left = left || state.tokens.curr; + right = right || state.tokens.next; + if (state.option.white && !left.comment) { + if (left.line === right.line) { + adjacent(left, right); + } + } + } + + function nonadjacent(left, right) { + if (state.option.white) { + left = left || state.tokens.curr; + right = right || state.tokens.next; + + if (left.value === ";" && right.value === ";") { + return; + } + + if (left.line === right.line && left.character === right.from) { + left.from += (left.character - left.from); + warning("W013", left, left.value); + } + } + } + + function nobreaknonadjacent(left, right) { + left = left || state.tokens.curr; + right = right || state.tokens.next; + if (!state.option.laxbreak && left.line !== right.line) { + warning("W014", right, right.id); + } else if (state.option.white) { + left = left || state.tokens.curr; + right = right || state.tokens.next; + if (left.character === right.from) { + left.from += (left.character - left.from); + warning("W013", left, left.value); + } + } + } + + function indentation(bias) { + if (!state.option.white && !state.option["(explicitIndent)"]) { + return; + } + + if (state.tokens.next.id === "(end)") { + return; + } + + var i = indent + (bias || 0); + if (state.tokens.next.from !== i) { + warning("W015", state.tokens.next, state.tokens.next.value, i, state.tokens.next.from); + } + } + + function nolinebreak(t) { + t = t || state.tokens.curr; + if (t.line !== state.tokens.next.line) { + warning("E022", t, t.value); + } + } + + + function comma(opts) { + opts = opts || {}; + + if (!opts.peek) { + if (state.tokens.curr.line !== state.tokens.next.line) { + if (!state.option.laxcomma) { + if (comma.first) { + warning("I001"); + comma.first = false; + } + warning("W014", state.tokens.curr, state.tokens.next.value); + } + } else if (!state.tokens.curr.comment && + state.tokens.curr.character !== state.tokens.next.from && state.option.white) { + state.tokens.curr.from += (state.tokens.curr.character - state.tokens.curr.from); + warning("W011", state.tokens.curr, state.tokens.curr.value); + } + + advance(","); + } + + // TODO: This is a temporary solution to fight against false-positives in + // arrays and objects with trailing commas (see GH-363). The best solution + // would be to extract all whitespace rules out of parser. + + if (state.tokens.next.value !== "]" && state.tokens.next.value !== "}") { + nonadjacent(state.tokens.curr, state.tokens.next); + } + + if (state.tokens.next.identifier && !(opts.property && state.option.inES5())) { + // Keywords that cannot follow a comma operator. + switch (state.tokens.next.value) { + case "break": + case "case": + case "catch": + case "continue": + case "default": + case "do": + case "else": + case "finally": + case "for": + case "if": + case "in": + case "instanceof": + case "return": + case "yield": + case "switch": + case "throw": + case "try": + case "var": + case "let": + case "while": + case "with": + error("E024", state.tokens.next, state.tokens.next.value); + return false; + } + } + + if (state.tokens.next.type === "(punctuator)") { + switch (state.tokens.next.value) { + case "}": + case "]": + case ",": + if (opts.allowTrailing) { + return true; + } + + /* falls through */ + case ")": + error("E024", state.tokens.next, state.tokens.next.value); + return false; + } + } + return true; + } + + // Functional constructors for making the symbols that will be inherited by + // tokens. + + function symbol(s, p) { + var x = state.syntax[s]; + if (!x || typeof x !== "object") { + state.syntax[s] = x = { + id: s, + lbp: p, + value: s + }; + } + return x; + } + + function delim(s) { + return symbol(s, 0); + } + + function stmt(s, f) { + var x = delim(s); + x.identifier = x.reserved = true; + x.fud = f; + return x; + } + + function blockstmt(s, f) { + var x = stmt(s, f); + x.block = true; + return x; + } + + function reserveName(x) { + var c = x.id.charAt(0); + if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z")) { + x.identifier = x.reserved = true; + } + return x; + } + + function prefix(s, f) { + var x = symbol(s, 150); + reserveName(x); + x.nud = (typeof f === "function") ? f : function () { + this.right = expression(150); + this.arity = "unary"; + if (this.id === "++" || this.id === "--") { + if (state.option.plusplus) { + warning("W016", this, this.id); + } else if ((!this.right.identifier || isReserved(this.right)) && + this.right.id !== "." && this.right.id !== "[") { + warning("W017", this); + } + } + return this; + }; + return x; + } + + function type(s, f) { + var x = delim(s); + x.type = s; + x.nud = f; + return x; + } + + function reserve(name, func) { + var x = type(name, func); + x.identifier = true; + x.reserved = true; + return x; + } + + function FutureReservedWord(name, meta) { + var x = type(name, (meta && meta.nud) || function () { + return this; + }); + + meta = meta || {}; + meta.isFutureReservedWord = true; + + x.value = name; + x.identifier = true; + x.reserved = true; + x.meta = meta; + + return x; + } + + function reservevar(s, v) { + return reserve(s, function () { + if (typeof v === "function") { + v(this); + } + return this; + }); + } + + function infix(s, f, p, w) { + var x = symbol(s, p); + reserveName(x); + x.led = function (left) { + if (!w) { + nobreaknonadjacent(state.tokens.prev, state.tokens.curr); + nonadjacent(state.tokens.curr, state.tokens.next); + } + if (s === "in" && left.id === "!") { + warning("W018", left, "!"); + } + if (typeof f === "function") { + return f(left, this); + } else { + this.left = left; + this.right = expression(p); + return this; + } + }; + return x; + } + + + function application(s) { + var x = symbol(s, 42); + + x.led = function (left) { + if (!state.option.inESNext()) { + warning("W104", state.tokens.curr, "arrow function syntax (=>)"); + } + + nobreaknonadjacent(state.tokens.prev, state.tokens.curr); + nonadjacent(state.tokens.curr, state.tokens.next); + + this.left = left; + this.right = doFunction(undefined, undefined, false, left); + return this; + }; + return x; + } + + function relation(s, f) { + var x = symbol(s, 100); + + x.led = function (left) { + nobreaknonadjacent(state.tokens.prev, state.tokens.curr); + nonadjacent(state.tokens.curr, state.tokens.next); + var right = expression(100); + + if (isIdentifier(left, "NaN") || isIdentifier(right, "NaN")) { + warning("W019", this); + } else if (f) { + f.apply(this, [left, right]); + } + + if (!left || !right) { + quit("E041", state.tokens.curr.line); + } + + if (left.id === "!") { + warning("W018", left, "!"); + } + + if (right.id === "!") { + warning("W018", right, "!"); + } + + this.left = left; + this.right = right; + return this; + }; + return x; + } + + function isPoorRelation(node) { + return node && + ((node.type === "(number)" && +node.value === 0) || + (node.type === "(string)" && node.value === "") || + (node.type === "null" && !state.option.eqnull) || + node.type === "true" || + node.type === "false" || + node.type === "undefined"); + } + + function assignop(s) { + symbol(s, 20).exps = true; + + return infix(s, function (left, that) { + that.left = left; + + if (left) { + if (predefined[left.value] === false && + scope[left.value]["(global)"] === true) { + warning("W020", left); + } else if (left["function"]) { + warning("W021", left, left.value); + } + + if (funct[left.value] === "const") { + error("E013", left, left.value); + } + + if (left.id === ".") { + if (!left.left) { + warning("E031", that); + } else if (left.left.value === "arguments" && !state.directive["use strict"]) { + warning("E031", that); + } + + that.right = expression(19); + return that; + } else if (left.id === "[") { + if (state.tokens.curr.left.first) { + state.tokens.curr.left.first.forEach(function (t) { + if (funct[t.value] === "const") { + error("E013", t, t.value); + } + }); + } else if (!left.left) { + warning("E031", that); + } else if (left.left.value === "arguments" && !state.directive["use strict"]) { + warning("E031", that); + } + that.right = expression(19); + return that; + } else if (left.identifier && !isReserved(left)) { + if (funct[left.value] === "exception") { + warning("W022", left); + } + that.right = expression(19); + return that; + } + + if (left === state.syntax["function"]) { + warning("W023", state.tokens.curr); + } + } + + error("E031", that); + }, 20); + } + + + function bitwise(s, f, p) { + var x = symbol(s, p); + reserveName(x); + x.led = (typeof f === "function") ? f : function (left) { + if (state.option.bitwise) { + warning("W016", this, this.id); + } + this.left = left; + this.right = expression(p); + return this; + }; + return x; + } + + + function bitwiseassignop(s) { + symbol(s, 20).exps = true; + return infix(s, function (left, that) { + if (state.option.bitwise) { + warning("W016", that, that.id); + } + nonadjacent(state.tokens.prev, state.tokens.curr); + nonadjacent(state.tokens.curr, state.tokens.next); + if (left) { + if (left.id === "." || left.id === "[" || + (left.identifier && !isReserved(left))) { + expression(19); + return that; + } + if (left === state.syntax["function"]) { + warning("W023", state.tokens.curr); + } + return that; + } + error("E031", that); + }, 20); + } + + + function suffix(s) { + var x = symbol(s, 150); + + x.led = function (left) { + if (state.option.plusplus) { + warning("W016", this, this.id); + } else if ((!left.identifier || isReserved(left)) && left.id !== "." && left.id !== "[") { + warning("W017", this); + } + + this.left = left; + return this; + }; + return x; + } + + // fnparam means that this identifier is being defined as a function + // argument (see identifier()) + // prop means that this identifier is that of an object property + + function optionalidentifier(fnparam, prop) { + if (!state.tokens.next.identifier) { + return; + } + + advance(); + + var curr = state.tokens.curr; + var meta = curr.meta || {}; + var val = state.tokens.curr.value; + + if (!isReserved(curr)) { + return val; + } + + if (prop) { + if (state.option.inES5() || meta.isFutureReservedWord) { + return val; + } + } + + if (fnparam && val === "undefined") { + return val; + } + + // Display an info message about reserved words as properties + // and ES5 but do it only once. + if (prop && !api.getCache("displayed:I002")) { + api.setCache("displayed:I002", true); + warning("I002"); + } + + warning("W024", state.tokens.curr, state.tokens.curr.id); + return val; + } + + // fnparam means that this identifier is being defined as a function + // argument + // prop means that this identifier is that of an object property + function identifier(fnparam, prop) { + var i = optionalidentifier(fnparam, prop); + if (i) { + return i; + } + if (state.tokens.curr.id === "function" && state.tokens.next.id === "(") { + warning("W025"); + } else { + error("E030", state.tokens.next, state.tokens.next.value); + } + } + + + function reachable(s) { + var i = 0, t; + if (state.tokens.next.id !== ";" || noreach) { + return; + } + for (;;) { + t = peek(i); + if (t.reach) { + return; + } + if (t.id !== "(endline)") { + if (t.id === "function") { + if (!state.option.latedef) { + break; + } + + warning("W026", t); + break; + } + + warning("W027", t, t.value, s); + break; + } + i += 1; + } + } + + + function statement(noindent) { + var values; + var i = indent, r, s = scope, t = state.tokens.next; + + if (t.id === ";") { + advance(";"); + return; + } + + // Is this a labelled statement? + var res = isReserved(t); + + // We're being more tolerant here: if someone uses + // a FutureReservedWord as a label, we warn but proceed + // anyway. + + if (res && t.meta && t.meta.isFutureReservedWord && peek().id === ":") { + warning("W024", t, t.id); + res = false; + } + + // detect a destructuring assignment + if (_.has(["[", "{"], t.value)) { + if (lookupBlockType().isDestAssign) { + if (!state.option.inESNext()) { + warning("W104", state.tokens.curr, "destructuring expression"); + } + values = destructuringExpression(); + values.forEach(function (tok) { + isundef(funct, "W117", tok.token, tok.id); + }); + advance("="); + destructuringExpressionMatch(values, expression(5, true)); + advance(";"); + return; + } + } + if (t.identifier && !res && peek().id === ":") { + advance(); + advance(":"); + scope = Object.create(s); + addlabel(t.value, "label"); + + if (!state.tokens.next.labelled && state.tokens.next.value !== "{") { + warning("W028", state.tokens.next, t.value, state.tokens.next.value); + } + + state.tokens.next.label = t.value; + t = state.tokens.next; + } + + // Is it a lonely block? + + if (t.id === "{") { + // Is it a switch case block? + // + // switch (foo) { + // case bar: { <= here. + // ... + // } + // } + var iscase = (funct["(verb)"] === "case" && state.tokens.curr.value === ":"); + block(true, true, false, false, iscase); + return; + } + + // Parse the statement. + + if (!noindent) { + indentation(); + } + r = expression(0, true); + + // Look for the final semicolon. + + if (!t.block) { + if (!state.option.expr && (!r || !r.exps)) { + warning("W030", state.tokens.curr); + } else if (state.option.nonew && r && r.left && r.id === "(" && r.left.id === "new") { + warning("W031", t); + } + + if (state.tokens.next.id !== ";") { + if (!state.option.asi) { + // If this is the last statement in a block that ends on + // the same line *and* option lastsemic is on, ignore the warning. + // Otherwise, complain about missing semicolon. + if (!state.option.lastsemic || state.tokens.next.id !== "}" || + state.tokens.next.line !== state.tokens.curr.line) { + warningAt("W033", state.tokens.curr.line, state.tokens.curr.character); + } + } + } else { + adjacent(state.tokens.curr, state.tokens.next); + advance(";"); + nonadjacent(state.tokens.curr, state.tokens.next); + } + } + + // Restore the indentation. + + indent = i; + scope = s; + return r; + } + + + function statements(startLine) { + var a = [], p; + + while (!state.tokens.next.reach && state.tokens.next.id !== "(end)") { + if (state.tokens.next.id === ";") { + p = peek(); + + if (!p || (p.id !== "(" && p.id !== "[")) { + warning("W032"); + } + + advance(";"); + } else { + a.push(statement(startLine === state.tokens.next.line)); + } + } + return a; + } + + + /* + * read all directives + * recognizes a simple form of asi, but always + * warns, if it is used + */ + function directives() { + var i, p, pn; + + for (;;) { + if (state.tokens.next.id === "(string)") { + p = peek(0); + if (p.id === "(endline)") { + i = 1; + do { + pn = peek(i); + i = i + 1; + } while (pn.id === "(endline)"); + + if (pn.id !== ";") { + if (pn.id !== "(string)" && pn.id !== "(number)" && + pn.id !== "(regexp)" && pn.identifier !== true && + pn.id !== "}") { + break; + } + warning("W033", state.tokens.next); + } else { + p = pn; + } + } else if (p.id === "}") { + // Directive with no other statements, warn about missing semicolon + warning("W033", p); + } else if (p.id !== ";") { + break; + } + + indentation(); + advance(); + if (state.directive[state.tokens.curr.value]) { + warning("W034", state.tokens.curr, state.tokens.curr.value); + } + + if (state.tokens.curr.value === "use strict") { + if (!state.option["(explicitNewcap)"]) + state.option.newcap = true; + state.option.undef = true; + } + + // there's no directive negation, so always set to true + state.directive[state.tokens.curr.value] = true; + + if (p.id === ";") { + advance(";"); + } + continue; + } + break; + } + } + + + /* + * Parses a single block. A block is a sequence of statements wrapped in + * braces. + * + * ordinary - true for everything but function bodies and try blocks. + * stmt - true if block can be a single statement (e.g. in if/for/while). + * isfunc - true if block is a function body + * isfatarrow - + * iscase - true if block is a switch case block + */ + function block(ordinary, stmt, isfunc, isfatarrow, iscase) { + var a, + b = inblock, + old_indent = indent, + m, + s = scope, + t, + line, + d; + + inblock = ordinary; + + if (!ordinary || !state.option.funcscope) + scope = Object.create(scope); + + nonadjacent(state.tokens.curr, state.tokens.next); + t = state.tokens.next; + + var metrics = funct["(metrics)"]; + metrics.nestedBlockDepth += 1; + metrics.verifyMaxNestedBlockDepthPerFunction(); + + if (state.tokens.next.id === "{") { + advance("{"); + + // create a new block scope + funct["(blockscope)"].stack(); + + line = state.tokens.curr.line; + if (state.tokens.next.id !== "}") { + indent += state.option.indent; + while (!ordinary && state.tokens.next.from > indent) { + indent += state.option.indent; + } + + if (isfunc) { + m = {}; + for (d in state.directive) { + if (_.has(state.directive, d)) { + m[d] = state.directive[d]; + } + } + directives(); + + if (state.option.strict && funct["(context)"]["(global)"]) { + if (!m["use strict"] && !state.directive["use strict"]) { + warning("E007"); + } + } + } + + a = statements(line); + + metrics.statementCount += a.length; + + if (isfunc) { + state.directive = m; + } + + indent -= state.option.indent; + if (line !== state.tokens.next.line) { + indentation(); + } + } else if (line !== state.tokens.next.line) { + indentation(); + } + advance("}", t); + + funct["(blockscope)"].unstack(); + + indent = old_indent; + } else if (!ordinary) { + if (isfunc) { + m = {}; + if (stmt && !isfatarrow && !state.option.inMoz(true)) { + error("W118", state.tokens.curr, "function closure expressions"); + } + + if (!stmt) { + for (d in state.directive) { + if (_.has(state.directive, d)) { + m[d] = state.directive[d]; + } + } + } + expression(5); + + if (state.option.strict && funct["(context)"]["(global)"]) { + if (!m["use strict"] && !state.directive["use strict"]) { + warning("E007"); + } + } + } else { + error("E021", state.tokens.next, "{", state.tokens.next.value); + } + } else { + + // check to avoid let declaration not within a block + funct["(nolet)"] = true; + + if (!stmt || state.option.curly) { + warning("W116", state.tokens.next, "{", state.tokens.next.value); + } + + noreach = true; + indent += state.option.indent; + // test indentation only if statement is in new line + a = [statement(state.tokens.next.line === state.tokens.curr.line)]; + indent -= state.option.indent; + noreach = false; + + delete funct["(nolet)"]; + } + // If it is a "break" in switch case, don't clear and let it propagate out. + if (!(iscase && funct["(verb)"] === "break")) funct["(verb)"] = null; + + if (!ordinary || !state.option.funcscope) scope = s; + inblock = b; + if (ordinary && state.option.noempty && (!a || a.length === 0)) { + warning("W035"); + } + metrics.nestedBlockDepth -= 1; + return a; + } + + + function countMember(m) { + if (membersOnly && typeof membersOnly[m] !== "boolean") { + warning("W036", state.tokens.curr, m); + } + if (typeof member[m] === "number") { + member[m] += 1; + } else { + member[m] = 1; + } + } + + + function note_implied(tkn) { + var name = tkn.value, line = tkn.line, a = implied[name]; + if (typeof a === "function") { + a = false; + } + + if (!a) { + a = [line]; + implied[name] = a; + } else if (a[a.length - 1] !== line) { + a.push(line); + } + } + + + // Build the syntax table by declaring the syntactic elements of the language. + + type("(number)", function () { + return this; + }); + + type("(string)", function () { + return this; + }); + + state.syntax["(identifier)"] = { + type: "(identifier)", + lbp: 0, + identifier: true, + nud: function () { + var v = this.value, + s = scope[v], + f; + + if (typeof s === "function") { + // Protection against accidental inheritance. + s = undefined; + } else if (typeof s === "boolean") { + f = funct; + funct = functions[0]; + addlabel(v, "var"); + s = funct; + funct = f; + } + var block; + if (_.has(funct, "(blockscope)")) { + block = funct["(blockscope)"].getlabel(v); + } + + // The name is in scope and defined in the current function. + if (funct === s || block) { + // Change 'unused' to 'var', and reject labels. + // the name is in a block scope + switch (block ? block[v]["(type)"] : funct[v]) { + case "unused": + if (block) block[v]["(type)"] = "var"; + else funct[v] = "var"; + break; + case "unction": + if (block) block[v]["(type)"] = "function"; + else funct[v] = "function"; + this["function"] = true; + break; + case "function": + this["function"] = true; + break; + case "label": + warning("W037", state.tokens.curr, v); + break; + } + } else if (funct["(global)"]) { + // The name is not defined in the function. If we are in the global + // scope, then we have an undefined variable. + // + // Operators typeof and delete do not raise runtime errors even if + // the base object of a reference is null so no need to display warning + // if we're inside of typeof or delete. + + if (typeof predefined[v] !== "boolean") { + // Attempting to subscript a null reference will throw an + // error, even within the typeof and delete operators + if (!(anonname === "typeof" || anonname === "delete") || + (state.tokens.next && (state.tokens.next.value === "." || + state.tokens.next.value === "["))) { + + // if we're in a list comprehension, variables are declared + // locally and used before being defined. So we check + // the presence of the given variable in the comp array + // before declaring it undefined. + + if (!funct["(comparray)"].check(v)) { + isundef(funct, "W117", state.tokens.curr, v); + } + } + } + + note_implied(state.tokens.curr); + } else { + // If the name is already defined in the current + // function, but not as outer, then there is a scope error. + + switch (funct[v]) { + case "closure": + case "function": + case "var": + case "unused": + warning("W038", state.tokens.curr, v); + break; + case "label": + warning("W037", state.tokens.curr, v); + break; + case "outer": + case "global": + break; + default: + // If the name is defined in an outer function, make an outer entry, + // and if it was unused, make it var. + if (s === true) { + funct[v] = true; + } else if (s === null) { + warning("W039", state.tokens.curr, v); + note_implied(state.tokens.curr); + } else if (typeof s !== "object") { + // Operators typeof and delete do not raise runtime errors even + // if the base object of a reference is null so no need to + // + // display warning if we're inside of typeof or delete. + // Attempting to subscript a null reference will throw an + // error, even within the typeof and delete operators + if (!(anonname === "typeof" || anonname === "delete") || + (state.tokens.next && + (state.tokens.next.value === "." || state.tokens.next.value === "["))) { + + isundef(funct, "W117", state.tokens.curr, v); + } + funct[v] = true; + note_implied(state.tokens.curr); + } else { + switch (s[v]) { + case "function": + case "unction": + this["function"] = true; + s[v] = "closure"; + funct[v] = s["(global)"] ? "global" : "outer"; + break; + case "var": + case "unused": + s[v] = "closure"; + funct[v] = s["(global)"] ? "global" : "outer"; + break; + case "closure": + funct[v] = s["(global)"] ? "global" : "outer"; + break; + case "label": + warning("W037", state.tokens.curr, v); + } + } + } + } + return this; + }, + led: function () { + error("E033", state.tokens.next, state.tokens.next.value); + } + }; + + type("(regexp)", function () { + return this; + }); + + // ECMAScript parser + + delim("(endline)"); + delim("(begin)"); + delim("(end)").reach = true; + delim("(error)").reach = true; + delim("}").reach = true; + delim(")"); + delim("]"); + delim("\"").reach = true; + delim("'").reach = true; + delim(";"); + delim(":").reach = true; + delim("#"); + + reserve("else"); + reserve("case").reach = true; + reserve("catch"); + reserve("default").reach = true; + reserve("finally"); + reservevar("arguments", function (x) { + if (state.directive["use strict"] && funct["(global)"]) { + warning("E008", x); + } + }); + reservevar("eval"); + reservevar("false"); + reservevar("Infinity"); + reservevar("null"); + reservevar("this", function (x) { + if (state.directive["use strict"] && !state.option.validthis && ((funct["(statement)"] && + funct["(name)"].charAt(0) > "Z") || funct["(global)"])) { + warning("W040", x); + } + }); + reservevar("true"); + reservevar("undefined"); + + assignop("=", "assign", 20); + assignop("+=", "assignadd", 20); + assignop("-=", "assignsub", 20); + assignop("*=", "assignmult", 20); + assignop("/=", "assigndiv", 20).nud = function () { + error("E014"); + }; + assignop("%=", "assignmod", 20); + + bitwiseassignop("&=", "assignbitand", 20); + bitwiseassignop("|=", "assignbitor", 20); + bitwiseassignop("^=", "assignbitxor", 20); + bitwiseassignop("<<=", "assignshiftleft", 20); + bitwiseassignop(">>=", "assignshiftright", 20); + bitwiseassignop(">>>=", "assignshiftrightunsigned", 20); + infix(",", function (left, that) { + var expr; + that.exprs = [left]; + if (!comma({peek: true})) { + return that; + } + while (true) { + if (!(expr = expression(5))) { + break; + } + that.exprs.push(expr); + if (state.tokens.next.value !== "," || !comma()) { + break; + } + } + return that; + }, 5, true); + infix("?", function (left, that) { + that.left = left; + that.right = expression(10); + advance(":"); + that["else"] = expression(10); + return that; + }, 30); + + infix("||", "or", 40); + infix("&&", "and", 50); + bitwise("|", "bitor", 70); + bitwise("^", "bitxor", 80); + bitwise("&", "bitand", 90); + relation("==", function (left, right) { + var eqnull = state.option.eqnull && (left.value === "null" || right.value === "null"); + + if (!eqnull && state.option.eqeqeq) + warning("W116", this, "===", "=="); + else if (isPoorRelation(left)) + warning("W041", this, "===", left.value); + else if (isPoorRelation(right)) + warning("W041", this, "===", right.value); + + return this; + }); + relation("==="); + relation("!=", function (left, right) { + var eqnull = state.option.eqnull && + (left.value === "null" || right.value === "null"); + + if (!eqnull && state.option.eqeqeq) { + warning("W116", this, "!==", "!="); + } else if (isPoorRelation(left)) { + warning("W041", this, "!==", left.value); + } else if (isPoorRelation(right)) { + warning("W041", this, "!==", right.value); + } + return this; + }); + relation("!=="); + relation("<"); + relation(">"); + relation("<="); + relation(">="); + bitwise("<<", "shiftleft", 120); + bitwise(">>", "shiftright", 120); + bitwise(">>>", "shiftrightunsigned", 120); + infix("in", "in", 120); + infix("instanceof", "instanceof", 120); + infix("+", function (left, that) { + var right = expression(130); + if (left && right && left.id === "(string)" && right.id === "(string)") { + left.value += right.value; + left.character = right.character; + if (!state.option.scripturl && reg.javascriptURL.test(left.value)) { + warning("W050", left); + } + return left; + } + that.left = left; + that.right = right; + return that; + }, 130); + prefix("+", "num"); + prefix("+++", function () { + warning("W007"); + this.right = expression(150); + this.arity = "unary"; + return this; + }); + infix("+++", function (left) { + warning("W007"); + this.left = left; + this.right = expression(130); + return this; + }, 130); + infix("-", "sub", 130); + prefix("-", "neg"); + prefix("---", function () { + warning("W006"); + this.right = expression(150); + this.arity = "unary"; + return this; + }); + infix("---", function (left) { + warning("W006"); + this.left = left; + this.right = expression(130); + return this; + }, 130); + infix("*", "mult", 140); + infix("/", "div", 140); + infix("%", "mod", 140); + + suffix("++", "postinc"); + prefix("++", "preinc"); + state.syntax["++"].exps = true; + + suffix("--", "postdec"); + prefix("--", "predec"); + state.syntax["--"].exps = true; + prefix("delete", function () { + var p = expression(5); + if (!p || (p.id !== "." && p.id !== "[")) { + warning("W051"); + } + this.first = p; + return this; + }).exps = true; + + prefix("~", function () { + if (state.option.bitwise) { + warning("W052", this, "~"); + } + expression(150); + return this; + }); + + prefix("...", function () { + if (!state.option.inESNext()) { + warning("W104", this, "spread/rest operator"); + } + if (!state.tokens.next.identifier) { + error("E030", state.tokens.next, state.tokens.next.value); + } + expression(150); + return this; + }); + + prefix("!", function () { + this.right = expression(150); + this.arity = "unary"; + + if (!this.right) { // '!' followed by nothing? Give up. + quit("E041", this.line || 0); + } + + if (bang[this.right.id] === true) { + warning("W018", this, "!"); + } + return this; + }); + + prefix("typeof", "typeof"); + prefix("new", function () { + var c = expression(155), i; + if (c && c.id !== "function") { + if (c.identifier) { + c["new"] = true; + switch (c.value) { + case "Number": + case "String": + case "Boolean": + case "Math": + case "JSON": + warning("W053", state.tokens.prev, c.value); + break; + case "Function": + if (!state.option.evil) { + warning("W054"); + } + break; + case "Date": + case "RegExp": + break; + default: + if (c.id !== "function") { + i = c.value.substr(0, 1); + if (state.option.newcap && (i < "A" || i > "Z") && !_.has(global, c.value)) { + warning("W055", state.tokens.curr); + } + } + } + } else { + if (c.id !== "." && c.id !== "[" && c.id !== "(") { + warning("W056", state.tokens.curr); + } + } + } else { + if (!state.option.supernew) + warning("W057", this); + } + adjacent(state.tokens.curr, state.tokens.next); + if (state.tokens.next.id !== "(" && !state.option.supernew) { + warning("W058", state.tokens.curr, state.tokens.curr.value); + } + this.first = c; + return this; + }); + state.syntax["new"].exps = true; + + prefix("void").exps = true; + + infix(".", function (left, that) { + adjacent(state.tokens.prev, state.tokens.curr); + nobreak(); + var m = identifier(false, true); + + if (typeof m === "string") { + countMember(m); + } + + that.left = left; + that.right = m; + + if (m && m === "hasOwnProperty" && state.tokens.next.value === "=") { + warning("W001"); + } + + if (left && left.value === "arguments" && (m === "callee" || m === "caller")) { + if (state.option.noarg) + warning("W059", left, m); + else if (state.directive["use strict"]) + error("E008"); + } else if (!state.option.evil && left && left.value === "document" && + (m === "write" || m === "writeln")) { + warning("W060", left); + } + + if (!state.option.evil && (m === "eval" || m === "execScript")) { + warning("W061"); + } + + return that; + }, 160, true); + + infix("(", function (left, that) { + if (state.tokens.prev.id !== "}" && state.tokens.prev.id !== ")") { + nobreak(state.tokens.prev, state.tokens.curr); + } + + nospace(); + if (state.option.immed && left && !left.immed && left.id === "function") { + warning("W062"); + } + + var n = 0; + var p = []; + + if (left) { + if (left.type === "(identifier)") { + if (left.value.match(/^[A-Z]([A-Z0-9_$]*[a-z][A-Za-z0-9_$]*)?$/)) { + if ("Number String Boolean Date Object".indexOf(left.value) === -1) { + if (left.value === "Math") { + warning("W063", left); + } else if (state.option.newcap) { + warning("W064", left); + } + } + } + } + } + + if (state.tokens.next.id !== ")") { + for (;;) { + p[p.length] = expression(10); + n += 1; + if (state.tokens.next.id !== ",") { + break; + } + comma(); + } + } + + advance(")"); + nospace(state.tokens.prev, state.tokens.curr); + + if (typeof left === "object") { + if (left.value === "parseInt" && n === 1) { + warning("W065", state.tokens.curr); + } + if (!state.option.evil) { + if (left.value === "eval" || left.value === "Function" || + left.value === "execScript") { + warning("W061", left); + + if (p[0] && [0].id === "(string)") { + addInternalSrc(left, p[0].value); + } + } else if (p[0] && p[0].id === "(string)" && + (left.value === "setTimeout" || + left.value === "setInterval")) { + warning("W066", left); + addInternalSrc(left, p[0].value); + + // window.setTimeout/setInterval + } else if (p[0] && p[0].id === "(string)" && + left.value === "." && + left.left.value === "window" && + (left.right === "setTimeout" || + left.right === "setInterval")) { + warning("W066", left); + addInternalSrc(left, p[0].value); + } + } + if (!left.identifier && left.id !== "." && left.id !== "[" && + left.id !== "(" && left.id !== "&&" && left.id !== "||" && + left.id !== "?") { + warning("W067", left); + } + } + + that.left = left; + return that; + }, 155, true).exps = true; + + prefix("(", function () { + nospace(); + var bracket, brackets = []; + var pn, pn1, i = 0; + + do { + pn = peek(i); + i += 1; + pn1 = peek(i); + i += 1; + } while (pn.value !== ")" && pn1.value !== "=>" && pn1.value !== ";" && pn1.type !== "(end)"); + + if (state.tokens.next.id === "function") { + state.tokens.next.immed = true; + } + + var exprs = []; + + if (state.tokens.next.id !== ")") { + for (;;) { + if (pn1.value === "=>" && state.tokens.next.value === "{") { + bracket = state.tokens.next; + bracket.left = destructuringExpression(); + brackets.push(bracket); + for (var t in bracket.left) { + exprs.push(bracket.left[t].token); + } + } else { + exprs.push(expression(5)); + } + if (state.tokens.next.id !== ",") { + break; + } + comma(); + } + } + + advance(")", this); + nospace(state.tokens.prev, state.tokens.curr); + if (state.option.immed && exprs[0] && exprs[0].id === "function") { + if (state.tokens.next.id !== "(" && + (state.tokens.next.id !== "." || (peek().value !== "call" && peek().value !== "apply"))) { + warning("W068", this); + } + } + + if (state.tokens.next.value === "=>") { + return exprs; + } + if (!exprs.length) { + return; + } + exprs[exprs.length - 1].paren = true; + if (exprs.length > 1) { + return Object.create(state.syntax[","], { exprs: { value: exprs } }); + } + return exprs[0]; + }); + + application("=>"); + + infix("[", function (left, that) { + nobreak(state.tokens.prev, state.tokens.curr); + nospace(); + var e = expression(5), s; + if (e && e.type === "(string)") { + if (!state.option.evil && (e.value === "eval" || e.value === "execScript")) { + warning("W061", that); + } + + countMember(e.value); + if (!state.option.sub && reg.identifier.test(e.value)) { + s = state.syntax[e.value]; + if (!s || !isReserved(s)) { + warning("W069", state.tokens.prev, e.value); + } + } + } + advance("]", that); + + if (e && e.value === "hasOwnProperty" && state.tokens.next.value === "=") { + warning("W001"); + } + + nospace(state.tokens.prev, state.tokens.curr); + that.left = left; + that.right = e; + return that; + }, 160, true); + + function comprehensiveArrayExpression() { + var res = {}; + res.exps = true; + funct["(comparray)"].stack(); + + res.right = expression(5); + advance("for"); + if (state.tokens.next.value === "each") { + advance("each"); + if (!state.option.inMoz(true)) { + warning("W118", state.tokens.curr, "for each"); + } + } + advance("("); + funct["(comparray)"].setState("define"); + res.left = expression(5); + advance(")"); + if (state.tokens.next.value === "if") { + advance("if"); + advance("("); + funct["(comparray)"].setState("filter"); + res.filter = expression(5); + advance(")"); + } + advance("]"); + funct["(comparray)"].unstack(); + return res; + } + + prefix("[", function () { + var blocktype = lookupBlockType(true); + if (blocktype.isCompArray) { + if (!state.option.inMoz(true)) { + warning("W118", state.tokens.curr, "array comprehension"); + } + return comprehensiveArrayExpression(); + } else if (blocktype.isDestAssign && !state.option.inESNext()) { + warning("W104", state.tokens.curr, "destructuring assignment"); + } + var b = state.tokens.curr.line !== state.tokens.next.line; + this.first = []; + if (b) { + indent += state.option.indent; + if (state.tokens.next.from === indent + state.option.indent) { + indent += state.option.indent; + } + } + while (state.tokens.next.id !== "(end)") { + while (state.tokens.next.id === ",") { + if (!state.option.inES5()) + warning("W070"); + advance(","); + } + if (state.tokens.next.id === "]") { + break; + } + if (b && state.tokens.curr.line !== state.tokens.next.line) { + indentation(); + } + this.first.push(expression(10)); + if (state.tokens.next.id === ",") { + comma({ allowTrailing: true }); + if (state.tokens.next.id === "]" && !state.option.inES5(true)) { + warning("W070", state.tokens.curr); + break; + } + } else { + break; + } + } + if (b) { + indent -= state.option.indent; + indentation(); + } + advance("]", this); + return this; + }, 160); + + + function property_name() { + var id = optionalidentifier(false, true); + + if (!id) { + if (state.tokens.next.id === "(string)") { + id = state.tokens.next.value; + advance(); + } else if (state.tokens.next.id === "(number)") { + id = state.tokens.next.value.toString(); + advance(); + } + } + + if (id === "hasOwnProperty") { + warning("W001"); + } + + return id; + } + + + function functionparams(parsed) { + var curr, next; + var params = []; + var ident; + var tokens = []; + var t; + + if (parsed) { + if (parsed instanceof Array) { + for (var i in parsed) { + curr = parsed[i]; + if (_.contains(["{", "["], curr.id)) { + for (t in curr.left) { + t = tokens[t]; + if (t.id) { + params.push(t.id); + addlabel(t.id, "unused", t.token); + } + } + } else if (curr.value === "...") { + if (!state.option.inESNext()) { + warning("W104", curr, "spread/rest operator"); + } + continue; + } else { + addlabel(curr.value, "unused", curr); + } + } + return params; + } else { + if (parsed.identifier === true) { + addlabel(parsed.value, "unused", parsed); + return [parsed]; + } + } + } + + next = state.tokens.next; + + advance("("); + nospace(); + + if (state.tokens.next.id === ")") { + advance(")"); + return; + } + + for (;;) { + if (_.contains(["{", "["], state.tokens.next.id)) { + tokens = destructuringExpression(); + for (t in tokens) { + t = tokens[t]; + if (t.id) { + params.push(t.id); + addlabel(t.id, "unused", t.token); + } + } + } else if (state.tokens.next.value === "...") { + if (!state.option.inESNext()) { + warning("W104", state.tokens.next, "spread/rest operator"); + } + advance("..."); + nospace(); + ident = identifier(true); + params.push(ident); + addlabel(ident, "unused", state.tokens.curr); + } else { + ident = identifier(true); + params.push(ident); + addlabel(ident, "unused", state.tokens.curr); + } + if (state.tokens.next.id === ",") { + comma(); + } else { + advance(")", next); + nospace(state.tokens.prev, state.tokens.curr); + return params; + } + } + } + + + function doFunction(name, statement, generator, fatarrowparams) { + var f; + var oldOption = state.option; + var oldIgnored = state.ignored; + var oldScope = scope; + + state.option = Object.create(state.option); + state.ignored = Object.create(state.ignored); + scope = Object.create(scope); + + funct = { + "(name)" : name || "\"" + anonname + "\"", + "(line)" : state.tokens.next.line, + "(character)" : state.tokens.next.character, + "(context)" : funct, + "(breakage)" : 0, + "(loopage)" : 0, + "(metrics)" : createMetrics(state.tokens.next), + "(scope)" : scope, + "(statement)" : statement, + "(tokens)" : {}, + "(blockscope)": funct["(blockscope)"], + "(comparray)" : funct["(comparray)"] + }; + + if (generator) { + funct["(generator)"] = true; + } + + f = funct; + state.tokens.curr.funct = funct; + + functions.push(funct); + + if (name) { + addlabel(name, "function"); + } + + funct["(params)"] = functionparams(fatarrowparams); + + funct["(metrics)"].verifyMaxParametersPerFunction(funct["(params)"]); + + block(false, true, true, fatarrowparams ? true:false); + + if (generator && funct["(generator)"] !== "yielded") { + error("E047", state.tokens.curr); + } + + funct["(metrics)"].verifyMaxStatementsPerFunction(); + funct["(metrics)"].verifyMaxComplexityPerFunction(); + funct["(unusedOption)"] = state.option.unused; + + scope = oldScope; + state.option = oldOption; + state.ignored = oldIgnored; + funct["(last)"] = state.tokens.curr.line; + funct["(lastcharacter)"] = state.tokens.curr.character; + funct = funct["(context)"]; + + return f; + } + + function createMetrics(functionStartToken) { + return { + statementCount: 0, + nestedBlockDepth: -1, + ComplexityCount: 1, + verifyMaxStatementsPerFunction: function () { + if (state.option.maxstatements && + this.statementCount > state.option.maxstatements) { + warning("W071", functionStartToken, this.statementCount); + } + }, + + verifyMaxParametersPerFunction: function (params) { + params = params || []; + + if (state.option.maxparams && params.length > state.option.maxparams) { + warning("W072", functionStartToken, params.length); + } + }, + + verifyMaxNestedBlockDepthPerFunction: function () { + if (state.option.maxdepth && + this.nestedBlockDepth > 0 && + this.nestedBlockDepth === state.option.maxdepth + 1) { + warning("W073", null, this.nestedBlockDepth); + } + }, + + verifyMaxComplexityPerFunction: function () { + var max = state.option.maxcomplexity; + var cc = this.ComplexityCount; + if (max && cc > max) { + warning("W074", functionStartToken, cc); + } + } + }; + } + + function increaseComplexityCount() { + funct["(metrics)"].ComplexityCount += 1; + } + + // Parse assignments that were found instead of conditionals. + // For example: if (a = 1) { ... } + + function checkCondAssignment(expr) { + var id = expr.id; + if (id === ",") { + expr = expr.exprs[expr.exprs.length - 1]; + id = expr.id; + } + switch (id) { + case "=": + case "+=": + case "-=": + case "*=": + case "%=": + case "&=": + case "|=": + case "^=": + case "/=": + if (!expr.paren && !state.option.boss) { + warning("W084"); + } + } + } + + + (function (x) { + x.nud = function (isclassdef) { + var b, f, i, p, t, g; + var props = {}; // All properties, including accessors + var tag = ""; + + function saveProperty(name, tkn) { + if (props[name] && _.has(props, name)) + warning("W075", state.tokens.next, i); + else + props[name] = {}; + + props[name].basic = true; + props[name].basictkn = tkn; + } + + function saveSetter(name, tkn) { + if (props[name] && _.has(props, name)) { + if (props[name].basic || props[name].setter) + warning("W075", state.tokens.next, i); + } else { + props[name] = {}; + } + + props[name].setter = true; + props[name].setterToken = tkn; + } + + function saveGetter(name) { + if (props[name] && _.has(props, name)) { + if (props[name].basic || props[name].getter) + warning("W075", state.tokens.next, i); + } else { + props[name] = {}; + } + + props[name].getter = true; + props[name].getterToken = state.tokens.curr; + } + + b = state.tokens.curr.line !== state.tokens.next.line; + if (b) { + indent += state.option.indent; + if (state.tokens.next.from === indent + state.option.indent) { + indent += state.option.indent; + } + } + + for (;;) { + if (state.tokens.next.id === "}") { + break; + } + + if (b) { + indentation(); + } + + if (isclassdef && state.tokens.next.value === "static") { + advance("static"); + tag = "static "; + } + + if (state.tokens.next.value === "get" && peek().id !== ":") { + advance("get"); + + if (!state.option.inES5(!isclassdef)) { + error("E034"); + } + + i = property_name(); + if (!i) { + error("E035"); + } + + // It is a Syntax Error if PropName of MethodDefinition is + // "constructor" and SpecialMethod of MethodDefinition is true. + if (isclassdef && i === "constructor") { + error("E049", state.tokens.next, "class getter method", i); + } + + saveGetter(tag + i); + t = state.tokens.next; + adjacent(state.tokens.curr, state.tokens.next); + f = doFunction(); + p = f["(params)"]; + + if (p) { + warning("W076", t, p[0], i); + } + + adjacent(state.tokens.curr, state.tokens.next); + } else if (state.tokens.next.value === "set" && peek().id !== ":") { + advance("set"); + + if (!state.option.inES5(!isclassdef)) { + error("E034"); + } + + i = property_name(); + if (!i) { + error("E035"); + } + + // It is a Syntax Error if PropName of MethodDefinition is + // "constructor" and SpecialMethod of MethodDefinition is true. + if (isclassdef && i === "constructor") { + error("E049", state.tokens.next, "class setter method", i); + } + + saveSetter(tag + i, state.tokens.next); + t = state.tokens.next; + adjacent(state.tokens.curr, state.tokens.next); + f = doFunction(); + p = f["(params)"]; + + if (!p || p.length !== 1) { + warning("W077", t, i); + } + } else { + g = false; + if (state.tokens.next.value === "*" && state.tokens.next.type === "(punctuator)") { + if (!state.option.inESNext()) { + warning("W104", state.tokens.next, "generator functions"); + } + advance("*"); + g = true; + } + i = property_name(); + saveProperty(tag + i, state.tokens.next); + + if (typeof i !== "string") { + break; + } + + if (state.tokens.next.value === "(") { + if (!state.option.inESNext()) { + warning("W104", state.tokens.curr, "concise methods"); + } + doFunction(i, undefined, g); + } else if (!isclassdef) { + advance(":"); + nonadjacent(state.tokens.curr, state.tokens.next); + expression(10); + } + } + // It is a Syntax Error if PropName of MethodDefinition is "prototype". + if (isclassdef && i === "prototype") { + error("E049", state.tokens.next, "class method", i); + } + + countMember(i); + if (isclassdef) { + tag = ""; + continue; + } + if (state.tokens.next.id === ",") { + comma({ allowTrailing: true, property: true }); + if (state.tokens.next.id === ",") { + warning("W070", state.tokens.curr); + } else if (state.tokens.next.id === "}" && !state.option.inES5(true)) { + warning("W070", state.tokens.curr); + } + } else { + break; + } + } + if (b) { + indent -= state.option.indent; + indentation(); + } + advance("}", this); + + // Check for lonely setters if in the ES5 mode. + if (state.option.inES5()) { + for (var name in props) { + if (_.has(props, name) && props[name].setter && !props[name].getter) { + warning("W078", props[name].setterToken); + } + } + } + return this; + }; + x.fud = function () { + error("E036", state.tokens.curr); + }; + }(delim("{"))); + + function destructuringExpression() { + var id, ids; + var identifiers = []; + if (!state.option.inESNext()) { + warning("W104", state.tokens.curr, "destructuring expression"); + } + var nextInnerDE = function () { + var ident; + if (_.contains(["[", "{"], state.tokens.next.value)) { + ids = destructuringExpression(); + for (var id in ids) { + id = ids[id]; + identifiers.push({ id: id.id, token: id.token }); + } + } else if (state.tokens.next.value === ",") { + identifiers.push({ id: null, token: state.tokens.curr }); + } else { + ident = identifier(); + if (ident) + identifiers.push({ id: ident, token: state.tokens.curr }); + } + }; + if (state.tokens.next.value === "[") { + advance("["); + nextInnerDE(); + while (state.tokens.next.value !== "]") { + advance(","); + nextInnerDE(); + } + advance("]"); + } else if (state.tokens.next.value === "{") { + advance("{"); + id = identifier(); + if (state.tokens.next.value === ":") { + advance(":"); + nextInnerDE(); + } else { + identifiers.push({ id: id, token: state.tokens.curr }); + } + while (state.tokens.next.value !== "}") { + advance(","); + id = identifier(); + if (state.tokens.next.value === ":") { + advance(":"); + nextInnerDE(); + } else { + identifiers.push({ id: id, token: state.tokens.curr }); + } + } + advance("}"); + } + return identifiers; + } + function destructuringExpressionMatch(tokens, value) { + if (value.first) { + _.zip(tokens, value.first).forEach(function (val) { + var token = val[0]; + var value = val[1]; + if (token && value) { + token.first = value; + } else if (token && token.first && !value) { + warning("W080", token.first, token.first.value); + } /* else { + XXX value is discarded: wouldn't it need a warning ? + } */ + }); + } + } + + var conststatement = stmt("const", function (prefix) { + var tokens, value; + // state variable to know if it is a lone identifier, or a destructuring statement. + var lone; + + if (!state.option.inESNext()) { + warning("W104", state.tokens.curr, "const"); + } + + this.first = []; + for (;;) { + var names = []; + nonadjacent(state.tokens.curr, state.tokens.next); + if (_.contains(["{", "["], state.tokens.next.value)) { + tokens = destructuringExpression(); + lone = false; + } else { + tokens = [ { id: identifier(), token: state.tokens.curr } ]; + lone = true; + } + for (var t in tokens) { + t = tokens[t]; + if (funct[t.id] === "const") { + warning("E011", null, t.id); + } + if (funct["(global)"] && predefined[t.id] === false) { + warning("W079", t.token, t.id); + } + if (t.id) { + addlabel(t.id, "const"); + names.push(t.token); + } + } + if (prefix) { + break; + } + + this.first = this.first.concat(names); + + if (state.tokens.next.id !== "=") { + warning("E012", state.tokens.curr, state.tokens.curr.value); + } + + if (state.tokens.next.id === "=") { + nonadjacent(state.tokens.curr, state.tokens.next); + advance("="); + nonadjacent(state.tokens.curr, state.tokens.next); + if (state.tokens.next.id === "undefined") { + warning("W080", state.tokens.prev, state.tokens.prev.value); + } + if (peek(0).id === "=" && state.tokens.next.identifier) { + error("E037", state.tokens.next, state.tokens.next.value); + } + value = expression(5); + if (lone) { + tokens[0].first = value; + } else { + destructuringExpressionMatch(names, value); + } + } + + if (state.tokens.next.id !== ",") { + break; + } + comma(); + } + return this; + }); + conststatement.exps = true; + var varstatement = stmt("var", function (prefix) { + // JavaScript does not have block scope. It only has function scope. So, + // declaring a variable in a block can have unexpected consequences. + var tokens, lone, value; + + if (funct["(onevar)"] && state.option.onevar) { + warning("W081"); + } else if (!funct["(global)"]) { + funct["(onevar)"] = true; + } + + this.first = []; + for (;;) { + var names = []; + nonadjacent(state.tokens.curr, state.tokens.next); + if (_.contains(["{", "["], state.tokens.next.value)) { + tokens = destructuringExpression(); + lone = false; + } else { + tokens = [ { id: identifier(), token: state.tokens.curr } ]; + lone = true; + } + for (var t in tokens) { + t = tokens[t]; + if (state.option.inESNext() && funct[t.id] === "const") { + warning("E011", null, t.id); + } + if (funct["(global)"] && predefined[t.id] === false) { + warning("W079", t.token, t.id); + } + if (t.id) { + addlabel(t.id, "unused", t.token); + names.push(t.token); + } + } + if (prefix) { + break; + } + + this.first = this.first.concat(names); + + if (state.tokens.next.id === "=") { + nonadjacent(state.tokens.curr, state.tokens.next); + advance("="); + nonadjacent(state.tokens.curr, state.tokens.next); + if (state.tokens.next.id === "undefined") { + warning("W080", state.tokens.prev, state.tokens.prev.value); + } + if (peek(0).id === "=" && state.tokens.next.identifier) { + error("E038", state.tokens.next, state.tokens.next.value); + } + value = expression(5); + if (lone) { + tokens[0].first = value; + } else { + destructuringExpressionMatch(names, value); + } + } + + if (state.tokens.next.id !== ",") { + break; + } + comma(); + } + return this; + }); + varstatement.exps = true; + var letstatement = stmt("let", function (prefix) { + var tokens, lone, value, letblock; + + if (!state.option.inESNext()) { + warning("W104", state.tokens.curr, "let"); + } + + if (state.tokens.next.value === "(") { + if (!state.option.inMoz(true)) { + warning("W118", state.tokens.next, "let block"); + } + advance("("); + funct["(blockscope)"].stack(); + letblock = true; + } else if (funct["(nolet)"]) { + error("E048", state.tokens.curr); + } + + if (funct["(onevar)"] && state.option.onevar) { + warning("W081"); + } else if (!funct["(global)"]) { + funct["(onevar)"] = true; + } + + this.first = []; + for (;;) { + var names = []; + nonadjacent(state.tokens.curr, state.tokens.next); + if (_.contains(["{", "["], state.tokens.next.value)) { + tokens = destructuringExpression(); + lone = false; + } else { + tokens = [ { id: identifier(), token: state.tokens.curr.value } ]; + lone = true; + } + for (var t in tokens) { + t = tokens[t]; + if (state.option.inESNext() && funct[t.id] === "const") { + warning("E011", null, t.id); + } + if (funct["(global)"] && predefined[t.id] === false) { + warning("W079", t.token, t.id); + } + if (t.id && !funct["(nolet)"]) { + addlabel(t.id, "unused", t.token, true); + names.push(t.token); + } + } + if (prefix) { + break; + } + + this.first = this.first.concat(names); + + if (state.tokens.next.id === "=") { + nonadjacent(state.tokens.curr, state.tokens.next); + advance("="); + nonadjacent(state.tokens.curr, state.tokens.next); + if (state.tokens.next.id === "undefined") { + warning("W080", state.tokens.prev, state.tokens.prev.value); + } + if (peek(0).id === "=" && state.tokens.next.identifier) { + error("E037", state.tokens.next, state.tokens.next.value); + } + value = expression(5); + if (lone) { + tokens[0].first = value; + } else { + destructuringExpressionMatch(names, value); + } + } + + if (state.tokens.next.id !== ",") { + break; + } + comma(); + } + if (letblock) { + advance(")"); + block(true, true); + this.block = true; + funct["(blockscope)"].unstack(); + } + + return this; + }); + letstatement.exps = true; + + blockstmt("class", function () { + return classdef.call(this, true); + }); + + function classdef(stmt) { + /*jshint validthis:true */ + if (!state.option.inESNext()) { + warning("W104", state.tokens.curr, "class"); + } + if (stmt) { + // BindingIdentifier + this.name = identifier(); + addlabel(this.name, "unused", state.tokens.curr); + } else if (state.tokens.next.identifier && state.tokens.next.value !== "extends") { + // BindingIdentifier(opt) + this.name = identifier(); + } + classtail(this); + return this; + } + + function classtail(c) { + var strictness = state.directive["use strict"]; + + // ClassHeritage(opt) + if (state.tokens.next.value === "extends") { + advance("extends"); + c.heritage = expression(10); + } + + // A ClassBody is always strict code. + state.directive["use strict"] = true; + advance("{"); + // ClassBody(opt) + c.body = state.syntax["{"].nud(true); + state.directive["use strict"] = strictness; + } + + blockstmt("function", function () { + var generator = false; + if (state.tokens.next.value === "*") { + advance("*"); + if (state.option.inESNext(true)) { + generator = true; + } else { + warning("W119", state.tokens.curr, "function*"); + } + } + if (inblock) { + warning("W082", state.tokens.curr); + + } + var i = identifier(); + if (funct[i] === "const") { + warning("E011", null, i); + } + adjacent(state.tokens.curr, state.tokens.next); + addlabel(i, "unction", state.tokens.curr); + + doFunction(i, { statement: true }, generator); + if (state.tokens.next.id === "(" && state.tokens.next.line === state.tokens.curr.line) { + error("E039"); + } + return this; + }); + + prefix("function", function () { + var generator = false; + if (state.tokens.next.value === "*") { + if (!state.option.inESNext()) { + warning("W119", state.tokens.curr, "function*"); + } + advance("*"); + generator = true; + } + var i = optionalidentifier(); + if (i || state.option.gcl) { + adjacent(state.tokens.curr, state.tokens.next); + } else { + nonadjacent(state.tokens.curr, state.tokens.next); + } + doFunction(i, undefined, generator); + if (!state.option.loopfunc && funct["(loopage)"]) { + warning("W083"); + } + return this; + }); + + blockstmt("if", function () { + var t = state.tokens.next; + increaseComplexityCount(); + state.condition = true; + advance("("); + nonadjacent(this, t); + nospace(); + checkCondAssignment(expression(0)); + advance(")", t); + state.condition = false; + nospace(state.tokens.prev, state.tokens.curr); + block(true, true); + if (state.tokens.next.id === "else") { + nonadjacent(state.tokens.curr, state.tokens.next); + advance("else"); + if (state.tokens.next.id === "if" || state.tokens.next.id === "switch") { + statement(true); + } else { + block(true, true); + } + } + return this; + }); + + blockstmt("try", function () { + var b; + + function doCatch() { + var oldScope = scope; + var e; + + advance("catch"); + nonadjacent(state.tokens.curr, state.tokens.next); + advance("("); + + scope = Object.create(oldScope); + + e = state.tokens.next.value; + if (state.tokens.next.type !== "(identifier)") { + e = null; + warning("E030", state.tokens.next, e); + } + + advance(); + + funct = { + "(name)" : "(catch)", + "(line)" : state.tokens.next.line, + "(character)": state.tokens.next.character, + "(context)" : funct, + "(breakage)" : funct["(breakage)"], + "(loopage)" : funct["(loopage)"], + "(scope)" : scope, + "(statement)": false, + "(metrics)" : createMetrics(state.tokens.next), + "(catch)" : true, + "(tokens)" : {}, + "(blockscope)": funct["(blockscope)"], + "(comparray)": funct["(comparray)"] + }; + + if (e) { + addlabel(e, "exception"); + } + + if (state.tokens.next.value === "if") { + if (!state.option.inMoz(true)) { + warning("W118", state.tokens.curr, "catch filter"); + } + advance("if"); + expression(0); + } + + advance(")"); + + state.tokens.curr.funct = funct; + functions.push(funct); + + block(false); + + scope = oldScope; + + funct["(last)"] = state.tokens.curr.line; + funct["(lastcharacter)"] = state.tokens.curr.character; + funct = funct["(context)"]; + } + + block(false); + + while (state.tokens.next.id === "catch") { + increaseComplexityCount(); + if (b && (!state.option.inMoz(true))) { + warning("W118", state.tokens.next, "multiple catch blocks"); + } + doCatch(); + b = true; + } + + if (state.tokens.next.id === "finally") { + advance("finally"); + block(false); + return; + } + + if (!b) { + error("E021", state.tokens.next, "catch", state.tokens.next.value); + } + + return this; + }); + + blockstmt("while", function () { + var t = state.tokens.next; + funct["(breakage)"] += 1; + funct["(loopage)"] += 1; + increaseComplexityCount(); + advance("("); + nonadjacent(this, t); + nospace(); + checkCondAssignment(expression(0)); + advance(")", t); + nospace(state.tokens.prev, state.tokens.curr); + block(true, true); + funct["(breakage)"] -= 1; + funct["(loopage)"] -= 1; + return this; + }).labelled = true; + + blockstmt("with", function () { + var t = state.tokens.next; + if (state.directive["use strict"]) { + error("E010", state.tokens.curr); + } else if (!state.option.withstmt) { + warning("W085", state.tokens.curr); + } + + advance("("); + nonadjacent(this, t); + nospace(); + expression(0); + advance(")", t); + nospace(state.tokens.prev, state.tokens.curr); + block(true, true); + + return this; + }); + + blockstmt("switch", function () { + var t = state.tokens.next, + g = false; + funct["(breakage)"] += 1; + advance("("); + nonadjacent(this, t); + nospace(); + checkCondAssignment(expression(0)); + advance(")", t); + nospace(state.tokens.prev, state.tokens.curr); + nonadjacent(state.tokens.curr, state.tokens.next); + t = state.tokens.next; + advance("{"); + nonadjacent(state.tokens.curr, state.tokens.next); + indent += state.option.indent; + this.cases = []; + + for (;;) { + switch (state.tokens.next.id) { + case "case": + switch (funct["(verb)"]) { + case "yield": + case "break": + case "case": + case "continue": + case "return": + case "switch": + case "throw": + break; + default: + // You can tell JSHint that you don't use break intentionally by + // adding a comment /* falls through */ on a line just before + // the next `case`. + if (!reg.fallsThrough.test(state.lines[state.tokens.next.line - 2])) { + warning("W086", state.tokens.curr, "case"); + } + } + indentation(-state.option.indent); + advance("case"); + this.cases.push(expression(20)); + increaseComplexityCount(); + g = true; + advance(":"); + funct["(verb)"] = "case"; + break; + case "default": + switch (funct["(verb)"]) { + case "yield": + case "break": + case "continue": + case "return": + case "throw": + break; + default: + // Do not display a warning if 'default' is the first statement or if + // there is a special /* falls through */ comment. + if (this.cases.length) { + if (!reg.fallsThrough.test(state.lines[state.tokens.next.line - 2])) { + warning("W086", state.tokens.curr, "default"); + } + } + } + indentation(-state.option.indent); + advance("default"); + g = true; + advance(":"); + break; + case "}": + indent -= state.option.indent; + indentation(); + advance("}", t); + funct["(breakage)"] -= 1; + funct["(verb)"] = undefined; + return; + case "(end)": + error("E023", state.tokens.next, "}"); + return; + default: + if (g) { + switch (state.tokens.curr.id) { + case ",": + error("E040"); + return; + case ":": + g = false; + statements(); + break; + default: + error("E025", state.tokens.curr); + return; + } + } else { + if (state.tokens.curr.id === ":") { + advance(":"); + error("E024", state.tokens.curr, ":"); + statements(); + } else { + error("E021", state.tokens.next, "case", state.tokens.next.value); + return; + } + } + } + } + }).labelled = true; + + stmt("debugger", function () { + if (!state.option.debug) { + warning("W087"); + } + return this; + }).exps = true; + + (function () { + var x = stmt("do", function () { + funct["(breakage)"] += 1; + funct["(loopage)"] += 1; + increaseComplexityCount(); + + this.first = block(true, true); + advance("while"); + var t = state.tokens.next; + nonadjacent(state.tokens.curr, t); + advance("("); + nospace(); + checkCondAssignment(expression(0)); + advance(")", t); + nospace(state.tokens.prev, state.tokens.curr); + funct["(breakage)"] -= 1; + funct["(loopage)"] -= 1; + return this; + }); + x.labelled = true; + x.exps = true; + }()); + + blockstmt("for", function () { + var s, t = state.tokens.next; + var letscope = false; + var foreachtok = null; + + if (t.value === "each") { + foreachtok = t; + advance("each"); + if (!state.option.inMoz(true)) { + warning("W118", state.tokens.curr, "for each"); + } + } + + funct["(breakage)"] += 1; + funct["(loopage)"] += 1; + increaseComplexityCount(); + advance("("); + nonadjacent(this, t); + nospace(); + + // what kind of for(…) statement it is? for(…of…)? for(…in…)? for(…;…;…)? + var nextop; // contains the token of the "in" or "of" operator + var i = 0; + var inof = ["in", "of"]; + do { + nextop = peek(i); + ++i; + } while (!_.contains(inof, nextop.value) && nextop.value !== ";" && + nextop.type !== "(end)"); + + // if we're in a for (… in|of …) statement + if (_.contains(inof, nextop.value)) { + if (!state.option.inESNext() && nextop.value === "of") { + error("W104", nextop, "for of"); + } + if (state.tokens.next.id === "var") { + advance("var"); + state.syntax["var"].fud.call(state.syntax["var"].fud, true); + } else if (state.tokens.next.id === "let") { + advance("let"); + // create a new block scope + letscope = true; + funct["(blockscope)"].stack(); + state.syntax["let"].fud.call(state.syntax["let"].fud, true); + } else { + switch (funct[state.tokens.next.value]) { + case "unused": + funct[state.tokens.next.value] = "var"; + break; + case "var": + break; + default: + if (!funct["(blockscope)"].getlabel(state.tokens.next.value)) + warning("W088", state.tokens.next, state.tokens.next.value); + } + advance(); + } + advance(nextop.value); + expression(20); + advance(")", t); + s = block(true, true); + if (state.option.forin && s && (s.length > 1 || typeof s[0] !== "object" || + s[0].value !== "if")) { + warning("W089", this); + } + funct["(breakage)"] -= 1; + funct["(loopage)"] -= 1; + } else { + if (foreachtok) { + error("E045", foreachtok); + } + if (state.tokens.next.id !== ";") { + if (state.tokens.next.id === "var") { + advance("var"); + state.syntax["var"].fud.call(state.syntax["var"].fud); + } else if (state.tokens.next.id === "let") { + advance("let"); + // create a new block scope + letscope = true; + funct["(blockscope)"].stack(); + state.syntax["let"].fud.call(state.syntax["let"].fud); + } else { + for (;;) { + expression(0, "for"); + if (state.tokens.next.id !== ",") { + break; + } + comma(); + } + } + } + nolinebreak(state.tokens.curr); + advance(";"); + if (state.tokens.next.id !== ";") { + checkCondAssignment(expression(0)); + } + nolinebreak(state.tokens.curr); + advance(";"); + if (state.tokens.next.id === ";") { + error("E021", state.tokens.next, ")", ";"); + } + if (state.tokens.next.id !== ")") { + for (;;) { + expression(0, "for"); + if (state.tokens.next.id !== ",") { + break; + } + comma(); + } + } + advance(")", t); + nospace(state.tokens.prev, state.tokens.curr); + block(true, true); + funct["(breakage)"] -= 1; + funct["(loopage)"] -= 1; + + } + // unstack loop blockscope + if (letscope) { + funct["(blockscope)"].unstack(); + } + return this; + }).labelled = true; + + + stmt("break", function () { + var v = state.tokens.next.value; + + if (funct["(breakage)"] === 0) + warning("W052", state.tokens.next, this.value); + + if (!state.option.asi) + nolinebreak(this); + + if (state.tokens.next.id !== ";") { + if (state.tokens.curr.line === state.tokens.next.line) { + if (funct[v] !== "label") { + warning("W090", state.tokens.next, v); + } else if (scope[v] !== funct) { + warning("W091", state.tokens.next, v); + } + this.first = state.tokens.next; + advance(); + } + } + reachable("break"); + return this; + }).exps = true; + + + stmt("continue", function () { + var v = state.tokens.next.value; + + if (funct["(breakage)"] === 0) + warning("W052", state.tokens.next, this.value); + + if (!state.option.asi) + nolinebreak(this); + + if (state.tokens.next.id !== ";") { + if (state.tokens.curr.line === state.tokens.next.line) { + if (funct[v] !== "label") { + warning("W090", state.tokens.next, v); + } else if (scope[v] !== funct) { + warning("W091", state.tokens.next, v); + } + this.first = state.tokens.next; + advance(); + } + } else if (!funct["(loopage)"]) { + warning("W052", state.tokens.next, this.value); + } + reachable("continue"); + return this; + }).exps = true; + + + stmt("return", function () { + if (this.line === state.tokens.next.line) { + if (state.tokens.next.id === "(regexp)") + warning("W092"); + + if (state.tokens.next.id !== ";" && !state.tokens.next.reach) { + nonadjacent(state.tokens.curr, state.tokens.next); + this.first = expression(0); + + if (this.first && + this.first.type === "(punctuator)" && this.first.value === "=" && !state.option.boss) { + warningAt("W093", this.first.line, this.first.character); + } + } + } else { + if (state.tokens.next.type === "(punctuator)" && + ["[", "{", "+", "-"].indexOf(state.tokens.next.value) > -1) { + nolinebreak(this); // always warn (Line breaking error) + } + } + reachable("return"); + return this; + }).exps = true; + + stmt("yield", function () { + if (state.option.inESNext(true) && funct["(generator)"] !== true) { + error("E046", state.tokens.curr, "yield"); + } else if (!state.option.inESNext()) { + warning("W104", state.tokens.curr, "yield"); + } + funct["(generator)"] = "yielded"; + if (this.line === state.tokens.next.line) { + if (state.tokens.next.id === "(regexp)") + warning("W092"); + + if (state.tokens.next.id !== ";" && !state.tokens.next.reach) { + nonadjacent(state.tokens.curr, state.tokens.next); + this.first = expression(0); + + if (this.first.type === "(punctuator)" && this.first.value === "=" && !state.option.boss) { + warningAt("W093", this.first.line, this.first.character); + } + } + } else if (!state.option.asi) { + nolinebreak(this); // always warn (Line breaking error) + } + return this; + }).exps = true; + + + stmt("throw", function () { + nolinebreak(this); + nonadjacent(state.tokens.curr, state.tokens.next); + this.first = expression(20); + reachable("throw"); + return this; + }).exps = true; + + // Future Reserved Words + + FutureReservedWord("abstract"); + FutureReservedWord("boolean"); + FutureReservedWord("byte"); + FutureReservedWord("char"); + FutureReservedWord("class", { es5: true, nud: classdef }); + FutureReservedWord("double"); + FutureReservedWord("enum", { es5: true }); + FutureReservedWord("export", { es5: true }); + FutureReservedWord("extends", { es5: true }); + FutureReservedWord("final"); + FutureReservedWord("float"); + FutureReservedWord("goto"); + FutureReservedWord("implements", { es5: true, strictOnly: true }); + FutureReservedWord("import", { es5: true }); + FutureReservedWord("int"); + FutureReservedWord("interface", { es5: true, strictOnly: true }); + FutureReservedWord("long"); + FutureReservedWord("native"); + FutureReservedWord("package", { es5: true, strictOnly: true }); + FutureReservedWord("private", { es5: true, strictOnly: true }); + FutureReservedWord("protected", { es5: true, strictOnly: true }); + FutureReservedWord("public", { es5: true, strictOnly: true }); + FutureReservedWord("short"); + FutureReservedWord("static", { es5: true, strictOnly: true }); + FutureReservedWord("super", { es5: true }); + FutureReservedWord("synchronized"); + FutureReservedWord("throws"); + FutureReservedWord("transient"); + FutureReservedWord("volatile"); + + // this function is used to determine wether a squarebracket or a curlybracket + // expression is a comprehension array, destructuring assignment or a json value. + + var lookupBlockType = function () { + var pn, pn1; + var i = 0; + var bracketStack = 0; + var ret = {}; + if (_.contains(["[", "{"], state.tokens.curr.value)) + bracketStack += 1; + if (_.contains(["[", "{"], state.tokens.next.value)) + bracketStack += 1; + if (_.contains(["]", "}"], state.tokens.next.value)) + bracketStack -= 1; + do { + pn = peek(i); + pn1 = peek(i + 1); + i = i + 1; + if (_.contains(["[", "{"], pn.value)) { + bracketStack += 1; + } else if (_.contains(["]", "}"], pn.value)) { + bracketStack -= 1; + } + if (pn.identifier && pn.value === "for" && bracketStack === 1) { + ret.isCompArray = true; + ret.notJson = true; + break; + } + if (_.contains(["}", "]"], pn.value) && pn1.value === "=") { + ret.isDestAssign = true; + ret.notJson = true; + break; + } + if (pn.value === ";") { + ret.isBlock = true; + ret.notJson = true; + } + } while (bracketStack > 0 && pn.id !== "(end)" && i < 15); + return ret; + }; + + // Check whether this function has been reached for a destructuring assign with undeclared values + function destructuringAssignOrJsonValue() { + // lookup for the assignment (esnext only) + // if it has semicolons, it is a block, so go parse it as a block + // or it's not a block, but there are assignments, check for undeclared variables + + var block = lookupBlockType(); + if (block.notJson) { + if (!state.option.inESNext() && block.isDestAssign) { + warning("W104", state.tokens.curr, "destructuring assignment"); + } + statements(); + // otherwise parse json value + } else { + state.option.laxbreak = true; + state.jsonMode = true; + jsonValue(); + } + } + + // array comprehension parsing function + // parses and defines the three states of the list comprehension in order + // to avoid defining global variables, but keeping them to the list comprehension scope + // only. The order of the states are as follows: + // * "use" which will be the returned iterative part of the list comprehension + // * "define" which will define the variables local to the list comprehension + // * "filter" which will help filter out values + + var arrayComprehension = function () { + var CompArray = function () { + this.mode = "use"; + this.variables = []; + }; + var _carrays = []; + var _current; + function declare(v) { + var l = _current.variables.filter(function (elt) { + // if it has, change its undef state + if (elt.value === v) { + elt.undef = false; + return v; + } + }).length; + return l !== 0; + } + function use(v) { + var l = _current.variables.filter(function (elt) { + // and if it has been defined + if (elt.value === v && !elt.undef) { + if (elt.unused === true) { + elt.unused = false; + } + return v; + } + }).length; + // otherwise we warn about it + return (l === 0); + } + return {stack: function () { + _current = new CompArray(); + _carrays.push(_current); + }, + unstack: function () { + _current.variables.filter(function (v) { + if (v.unused) + warning("W098", v.token, v.value); + if (v.undef) + isundef(v.funct, "W117", v.token, v.value); + }); + _carrays.splice(_carrays[_carrays.length - 1], 1); + _current = _carrays[_carrays.length - 1]; + }, + setState: function (s) { + if (_.contains(["use", "define", "filter"], s)) + _current.mode = s; + }, + check: function (v) { + // When we are in "use" state of the list comp, we enqueue that var + if (_current && _current.mode === "use") { + _current.variables.push({funct: funct, + token: state.tokens.curr, + value: v, + undef: true, + unused: false}); + return true; + // When we are in "define" state of the list comp, + } else if (_current && _current.mode === "define") { + // check if the variable has been used previously + if (!declare(v)) { + _current.variables.push({funct: funct, + token: state.tokens.curr, + value: v, + undef: false, + unused: true}); + } + return true; + // When we are in "filter" state, + } else if (_current && _current.mode === "filter") { + // we check whether current variable has been declared + if (use(v)) { + // if not we warn about it + isundef(funct, "W117", state.tokens.curr, v); + } + return true; + } + return false; + } + }; + }; + + + // Parse JSON + + function jsonValue() { + + function jsonObject() { + var o = {}, t = state.tokens.next; + advance("{"); + if (state.tokens.next.id !== "}") { + for (;;) { + if (state.tokens.next.id === "(end)") { + error("E026", state.tokens.next, t.line); + } else if (state.tokens.next.id === "}") { + warning("W094", state.tokens.curr); + break; + } else if (state.tokens.next.id === ",") { + error("E028", state.tokens.next); + } else if (state.tokens.next.id !== "(string)") { + warning("W095", state.tokens.next, state.tokens.next.value); + } + if (o[state.tokens.next.value] === true) { + warning("W075", state.tokens.next, state.tokens.next.value); + } else if ((state.tokens.next.value === "__proto__" && + !state.option.proto) || (state.tokens.next.value === "__iterator__" && + !state.option.iterator)) { + warning("W096", state.tokens.next, state.tokens.next.value); + } else { + o[state.tokens.next.value] = true; + } + advance(); + advance(":"); + jsonValue(); + if (state.tokens.next.id !== ",") { + break; + } + advance(","); + } + } + advance("}"); + } + + function jsonArray() { + var t = state.tokens.next; + advance("["); + if (state.tokens.next.id !== "]") { + for (;;) { + if (state.tokens.next.id === "(end)") { + error("E027", state.tokens.next, t.line); + } else if (state.tokens.next.id === "]") { + warning("W094", state.tokens.curr); + break; + } else if (state.tokens.next.id === ",") { + error("E028", state.tokens.next); + } + jsonValue(); + if (state.tokens.next.id !== ",") { + break; + } + advance(","); + } + } + advance("]"); + } + + switch (state.tokens.next.id) { + case "{": + jsonObject(); + break; + case "[": + jsonArray(); + break; + case "true": + case "false": + case "null": + case "(number)": + case "(string)": + advance(); + break; + case "-": + advance("-"); + if (state.tokens.curr.character !== state.tokens.next.from) { + warning("W011", state.tokens.curr); + } + adjacent(state.tokens.curr, state.tokens.next); + advance("(number)"); + break; + default: + error("E003", state.tokens.next); + } + } + + var blockScope = function () { + var _current = {}; + var _variables = [_current]; + + function _checkBlockLabels() { + for (var t in _current) { + if (_current[t]["(type)"] === "unused") { + if (state.option.unused) { + var tkn = _current[t]["(token)"]; + var line = tkn.line; + var chr = tkn.character; + warningAt("W098", line, chr, t); + } + } + } + } + + return { + stack: function () { + _current = {}; + _variables.push(_current); + }, + + unstack: function () { + _checkBlockLabels(); + _variables.splice(_variables.length - 1, 1); + _current = _.last(_variables); + }, + + getlabel: function (l) { + for (var i = _variables.length - 1 ; i >= 0; --i) { + if (_.has(_variables[i], l)) { + return _variables[i]; + } + } + }, + + current: { + has: function (t) { + return _.has(_current, t); + }, + add: function (t, type, tok) { + _current[t] = { "(type)" : type, + "(token)": tok }; + } + } + }; + }; + + // The actual JSHINT function itself. + var itself = function (s, o, g) { + var a, i, k, x; + var optionKeys; + var newOptionObj = {}; + var newIgnoredObj = {}; + + state.reset(); + + if (o && o.scope) { + JSHINT.scope = o.scope; + } else { + JSHINT.errors = []; + JSHINT.undefs = []; + JSHINT.internals = []; + JSHINT.blacklist = {}; + JSHINT.scope = "(main)"; + } + + predefined = Object.create(null); + combine(predefined, vars.ecmaIdentifiers); + combine(predefined, vars.reservedVars); + + combine(predefined, g || {}); + + declared = Object.create(null); + exported = Object.create(null); + + if (o) { + a = o.predef; + if (a) { + if (!Array.isArray(a) && typeof a === "object") { + a = Object.keys(a); + } + + a.forEach(function (item) { + var slice, prop; + + if (item[0] === "-") { + slice = item.slice(1); + JSHINT.blacklist[slice] = slice; + } else { + prop = Object.getOwnPropertyDescriptor(o.predef, item); + predefined[item] = prop ? prop.value : false; + } + }); + } + + optionKeys = Object.keys(o); + for (x = 0; x < optionKeys.length; x++) { + if (/^-W\d{3}$/g.test(optionKeys[x])) { + newIgnoredObj[optionKeys[x].slice(1)] = true; + } else { + newOptionObj[optionKeys[x]] = o[optionKeys[x]]; + + if (optionKeys[x] === "newcap" && o[optionKeys[x]] === false) + newOptionObj["(explicitNewcap)"] = true; + + if (optionKeys[x] === "indent") + newOptionObj["(explicitIndent)"] = o[optionKeys[x]] === false ? false : true; + } + } + } + + state.option = newOptionObj; + state.ignored = newIgnoredObj; + + state.option.indent = state.option.indent || 4; + state.option.maxerr = state.option.maxerr || 50; + + indent = 1; + global = Object.create(predefined); + scope = global; + funct = { + "(global)": true, + "(name)": "(global)", + "(scope)": scope, + "(breakage)": 0, + "(loopage)": 0, + "(tokens)": {}, + "(metrics)": createMetrics(state.tokens.next), + "(blockscope)": blockScope(), + "(comparray)": arrayComprehension() + }; + functions = [funct]; + urls = []; + stack = null; + member = {}; + membersOnly = null; + implied = {}; + inblock = false; + lookahead = []; + warnings = 0; + unuseds = []; + + if (!isString(s) && !Array.isArray(s)) { + errorAt("E004", 0); + return false; + } + + api = { + get isJSON() { + return state.jsonMode; + }, + + getOption: function (name) { + return state.option[name] || null; + }, + + getCache: function (name) { + return state.cache[name]; + }, + + setCache: function (name, value) { + state.cache[name] = value; + }, + + warn: function (code, data) { + warningAt.apply(null, [ code, data.line, data.char ].concat(data.data)); + }, + + on: function (names, listener) { + names.split(" ").forEach(function (name) { + emitter.on(name, listener); + }.bind(this)); + } + }; + + emitter.removeAllListeners(); + (extraModules || []).forEach(function (func) { + func(api); + }); + + state.tokens.prev = state.tokens.curr = state.tokens.next = state.syntax["(begin)"]; + + lex = new Lexer(s); + + lex.on("warning", function (ev) { + warningAt.apply(null, [ ev.code, ev.line, ev.character].concat(ev.data)); + }); + + lex.on("error", function (ev) { + errorAt.apply(null, [ ev.code, ev.line, ev.character ].concat(ev.data)); + }); + + lex.on("fatal", function (ev) { + quit("E041", ev.line, ev.from); + }); + + lex.on("Identifier", function (ev) { + emitter.emit("Identifier", ev); + }); + + lex.on("String", function (ev) { + emitter.emit("String", ev); + }); + + lex.on("Number", function (ev) { + emitter.emit("Number", ev); + }); + + lex.start(); + + // Check options + for (var name in o) { + if (_.has(o, name)) { + checkOption(name, state.tokens.curr); + } + } + + assume(); + + // combine the passed globals after we've assumed all our options + combine(predefined, g || {}); + + //reset values + comma.first = true; + + try { + advance(); + switch (state.tokens.next.id) { + case "{": + case "[": + destructuringAssignOrJsonValue(); + break; + default: + directives(); + + if (state.directive["use strict"]) { + if (!state.option.globalstrict && !state.option.node) { + warning("W097", state.tokens.prev); + } + } + + statements(); + } + advance((state.tokens.next && state.tokens.next.value !== ".") ? "(end)" : undefined); + funct["(blockscope)"].unstack(); + + var markDefined = function (name, context) { + do { + if (typeof context[name] === "string") { + // JSHINT marks unused variables as 'unused' and + // unused function declaration as 'unction'. This + // code changes such instances back 'var' and + // 'closure' so that the code in JSHINT.data() + // doesn't think they're unused. + + if (context[name] === "unused") + context[name] = "var"; + else if (context[name] === "unction") + context[name] = "closure"; + + return true; + } + + context = context["(context)"]; + } while (context); + + return false; + }; + + var clearImplied = function (name, line) { + if (!implied[name]) + return; + + var newImplied = []; + for (var i = 0; i < implied[name].length; i += 1) { + if (implied[name][i] !== line) + newImplied.push(implied[name][i]); + } + + if (newImplied.length === 0) + delete implied[name]; + else + implied[name] = newImplied; + }; + + var warnUnused = function (name, tkn, type, unused_opt) { + var line = tkn.line; + var chr = tkn.character; + + if (unused_opt === undefined) { + unused_opt = state.option.unused; + } + + if (unused_opt === true) { + unused_opt = "last-param"; + } + + var warnable_types = { + "vars": ["var"], + "last-param": ["var", "param"], + "strict": ["var", "param", "last-param"] + }; + + if (unused_opt) { + if (warnable_types[unused_opt] && warnable_types[unused_opt].indexOf(type) !== -1) { + warningAt("W098", line, chr, name); + } + } + + unuseds.push({ + name: name, + line: line, + character: chr + }); + }; + + var checkUnused = function (func, key) { + var type = func[key]; + var tkn = func["(tokens)"][key]; + + if (key.charAt(0) === "(") + return; + + if (type !== "unused" && type !== "unction") + return; + + // Params are checked separately from other variables. + if (func["(params)"] && func["(params)"].indexOf(key) !== -1) + return; + + // Variable is in global scope and defined as exported. + if (func["(global)"] && _.has(exported, key)) { + return; + } + + warnUnused(key, tkn, "var"); + }; + + // Check queued 'x is not defined' instances to see if they're still undefined. + for (i = 0; i < JSHINT.undefs.length; i += 1) { + k = JSHINT.undefs[i].slice(0); + + if (markDefined(k[2].value, k[0])) { + clearImplied(k[2].value, k[2].line); + } else if (state.option.undef) { + warning.apply(warning, k.slice(1)); + } + } + + functions.forEach(function (func) { + if (func["(unusedOption)"] === false) { + return; + } + + for (var key in func) { + if (_.has(func, key)) { + checkUnused(func, key); + } + } + + if (!func["(params)"]) + return; + + var params = func["(params)"].slice(); + var param = params.pop(); + var type, unused_opt; + + while (param) { + type = func[param]; + unused_opt = func["(unusedOption)"] || state.option.unused; + unused_opt = unused_opt === true ? "last-param" : unused_opt; + + // 'undefined' is a special case for (function (window, undefined) { ... })(); + // patterns. + + if (param === "undefined") + return; + + if (type === "unused" || type === "unction") { + warnUnused(param, func["(tokens)"][param], "param", func["(unusedOption)"]); + } else if (unused_opt === "last-param") { + return; + } + + param = params.pop(); + } + }); + + for (var key in declared) { + if (_.has(declared, key) && !_.has(global, key)) { + warnUnused(key, declared[key], "var"); + } + } + + } catch (err) { + if (err && err.name === "JSHintError") { + var nt = state.tokens.next || {}; + JSHINT.errors.push({ + scope : "(main)", + raw : err.raw, + reason : err.message, + line : err.line || nt.line, + character : err.character || nt.from + }, null); + } else { + throw err; + } + } + + // Loop over the listed "internals", and check them as well. + + if (JSHINT.scope === "(main)") { + o = o || {}; + + for (i = 0; i < JSHINT.internals.length; i += 1) { + k = JSHINT.internals[i]; + o.scope = k.elem; + itself(k.value, o, g); + } + } + + return JSHINT.errors.length === 0; + }; + + // Modules. + itself.addModule = function (func) { + extraModules.push(func); + }; + + itself.addModule(style.register); + + // Data summary. + itself.data = function () { + var data = { + functions: [], + options: state.option + }; + var implieds = []; + var members = []; + var fu, f, i, j, n, globals; + + if (itself.errors.length) { + data.errors = itself.errors; + } + + if (state.jsonMode) { + data.json = true; + } + + for (n in implied) { + if (_.has(implied, n)) { + implieds.push({ + name: n, + line: implied[n] + }); + } + } + + if (implieds.length > 0) { + data.implieds = implieds; + } + + if (urls.length > 0) { + data.urls = urls; + } + + globals = Object.keys(scope); + if (globals.length > 0) { + data.globals = globals; + } + + for (i = 1; i < functions.length; i += 1) { + f = functions[i]; + fu = {}; + + for (j = 0; j < functionicity.length; j += 1) { + fu[functionicity[j]] = []; + } + + for (j = 0; j < functionicity.length; j += 1) { + if (fu[functionicity[j]].length === 0) { + delete fu[functionicity[j]]; + } + } + + fu.name = f["(name)"]; + fu.param = f["(params)"]; + fu.line = f["(line)"]; + fu.character = f["(character)"]; + fu.last = f["(last)"]; + fu.lastcharacter = f["(lastcharacter)"]; + data.functions.push(fu); + } + + if (unuseds.length > 0) { + data.unused = unuseds; + } + + members = []; + for (n in member) { + if (typeof member[n] === "number") { + data.member = member; + break; + } + } + + return data; + }; + + itself.jshint = itself; + + return itself; +}()); + +// Make JSHINT a Node module, if possible. +if (typeof exports === "object" && exports) { + exports.JSHINT = JSHINT; +} + +})() +},{"events":2,"../shared/vars.js":3,"./lex.js":10,"./reg.js":6,"./state.js":4,"../shared/messages.js":12,"./style.js":5,"console-browserify":7,"underscore":11}],12:[function(require,module,exports){ +(function(){"use strict"; + +var _ = require("underscore"); + +var errors = { + // JSHint options + E001: "Bad option: '{a}'.", + E002: "Bad option value.", + + // JSHint input + E003: "Expected a JSON value.", + E004: "Input is neither a string nor an array of strings.", + E005: "Input is empty.", + E006: "Unexpected early end of program.", + + // Strict mode + E007: "Missing \"use strict\" statement.", + E008: "Strict violation.", + E009: "Option 'validthis' can't be used in a global scope.", + E010: "'with' is not allowed in strict mode.", + + // Constants + E011: "const '{a}' has already been declared.", + E012: "const '{a}' is initialized to 'undefined'.", + E013: "Attempting to override '{a}' which is a constant.", + + // Regular expressions + E014: "A regular expression literal can be confused with '/='.", + E015: "Unclosed regular expression.", + E016: "Invalid regular expression.", + + // Tokens + E017: "Unclosed comment.", + E018: "Unbegun comment.", + E019: "Unmatched '{a}'.", + E020: "Expected '{a}' to match '{b}' from line {c} and instead saw '{d}'.", + E021: "Expected '{a}' and instead saw '{b}'.", + E022: "Line breaking error '{a}'.", + E023: "Missing '{a}'.", + E024: "Unexpected '{a}'.", + E025: "Missing ':' on a case clause.", + E026: "Missing '}' to match '{' from line {a}.", + E027: "Missing ']' to match '[' form line {a}.", + E028: "Illegal comma.", + E029: "Unclosed string.", + + // Everything else + E030: "Expected an identifier and instead saw '{a}'.", + E031: "Bad assignment.", // FIXME: Rephrase + E032: "Expected a small integer or 'false' and instead saw '{a}'.", + E033: "Expected an operator and instead saw '{a}'.", + E034: "get/set are ES5 features.", + E035: "Missing property name.", + E036: "Expected to see a statement and instead saw a block.", + E037: "Constant {a} was not declared correctly.", + E038: "Variable {a} was not declared correctly.", + E039: "Function declarations are not invocable. Wrap the whole function invocation in parens.", + E040: "Each value should have its own case label.", + E041: "Unrecoverable syntax error.", + E042: "Stopping.", + E043: "Too many errors.", + E044: "'{a}' is already defined and can't be redefined.", + E045: "Invalid for each loop.", + E046: "A yield statement shall be within a generator function (with syntax: `function*`)", + E047: "A generator function shall contain a yield statement.", + E048: "Let declaration not directly within block.", + E049: "A {a} cannot be named '{b}'." +}; + +var warnings = { + W001: "'hasOwnProperty' is a really bad name.", + W002: "Value of '{a}' may be overwritten in IE.", + W003: "'{a}' was used before it was defined.", + W004: "'{a}' is already defined.", + W005: "A dot following a number can be confused with a decimal point.", + W006: "Confusing minuses.", + W007: "Confusing pluses.", + W008: "A leading decimal point can be confused with a dot: '{a}'.", + W009: "The array literal notation [] is preferrable.", + W010: "The object literal notation {} is preferrable.", + W011: "Unexpected space after '{a}'.", + W012: "Unexpected space before '{a}'.", + W013: "Missing space after '{a}'.", + W014: "Bad line breaking before '{a}'.", + W015: "Expected '{a}' to have an indentation at {b} instead at {c}.", + W016: "Unexpected use of '{a}'.", + W017: "Bad operand.", + W018: "Confusing use of '{a}'.", + W019: "Use the isNaN function to compare with NaN.", + W020: "Read only.", + W021: "'{a}' is a function.", + W022: "Do not assign to the exception parameter.", + W023: "Expected an identifier in an assignment and instead saw a function invocation.", + W024: "Expected an identifier and instead saw '{a}' (a reserved word).", + W025: "Missing name in function declaration.", + W026: "Inner functions should be listed at the top of the outer function.", + W027: "Unreachable '{a}' after '{b}'.", + W028: "Label '{a}' on {b} statement.", + W030: "Expected an assignment or function call and instead saw an expression.", + W031: "Do not use 'new' for side effects.", + W032: "Unnecessary semicolon.", + W033: "Missing semicolon.", + W034: "Unnecessary directive \"{a}\".", + W035: "Empty block.", + W036: "Unexpected /*member '{a}'.", + W037: "'{a}' is a statement label.", + W038: "'{a}' used out of scope.", + W039: "'{a}' is not allowed.", + W040: "Possible strict violation.", + W041: "Use '{a}' to compare with '{b}'.", + W042: "Avoid EOL escaping.", + W043: "Bad escaping of EOL. Use option multistr if needed.", + W044: "Bad or unnecessary escaping.", + W045: "Bad number '{a}'.", + W046: "Don't use extra leading zeros '{a}'.", + W047: "A trailing decimal point can be confused with a dot: '{a}'.", + W048: "Unexpected control character in regular expression.", + W049: "Unexpected escaped character '{a}' in regular expression.", + W050: "JavaScript URL.", + W051: "Variables should not be deleted.", + W052: "Unexpected '{a}'.", + W053: "Do not use {a} as a constructor.", + W054: "The Function constructor is a form of eval.", + W055: "A constructor name should start with an uppercase letter.", + W056: "Bad constructor.", + W057: "Weird construction. Is 'new' unnecessary?", + W058: "Missing '()' invoking a constructor.", + W059: "Avoid arguments.{a}.", + W060: "document.write can be a form of eval.", + W061: "eval can be harmful.", + W062: "Wrap an immediate function invocation in parens " + + "to assist the reader in understanding that the expression " + + "is the result of a function, and not the function itself.", + W063: "Math is not a function.", + W064: "Missing 'new' prefix when invoking a constructor.", + W065: "Missing radix parameter.", + W066: "Implied eval. Consider passing a function instead of a string.", + W067: "Bad invocation.", + W068: "Wrapping non-IIFE function literals in parens is unnecessary.", + W069: "['{a}'] is better written in dot notation.", + W070: "Extra comma. (it breaks older versions of IE)", + W071: "This function has too many statements. ({a})", + W072: "This function has too many parameters. ({a})", + W073: "Blocks are nested too deeply. ({a})", + W074: "This function's cyclomatic complexity is too high. ({a})", + W075: "Duplicate key '{a}'.", + W076: "Unexpected parameter '{a}' in get {b} function.", + W077: "Expected a single parameter in set {a} function.", + W078: "Setter is defined without getter.", + W079: "Redefinition of '{a}'.", + W080: "It's not necessary to initialize '{a}' to 'undefined'.", + W081: "Too many var statements.", + W082: "Function declarations should not be placed in blocks. " + + "Use a function expression or move the statement to the top of " + + "the outer function.", + W083: "Don't make functions within a loop.", + W084: "Expected a conditional expression and instead saw an assignment.", + W085: "Don't use 'with'.", + W086: "Expected a 'break' statement before '{a}'.", + W087: "Forgotten 'debugger' statement?", + W088: "Creating global 'for' variable. Should be 'for (var {a} ...'.", + W089: "The body of a for in should be wrapped in an if statement to filter " + + "unwanted properties from the prototype.", + W090: "'{a}' is not a statement label.", + W091: "'{a}' is out of scope.", + W092: "Wrap the /regexp/ literal in parens to disambiguate the slash operator.", + W093: "Did you mean to return a conditional instead of an assignment?", + W094: "Unexpected comma.", + W095: "Expected a string and instead saw {a}.", + W096: "The '{a}' key may produce unexpected results.", + W097: "Use the function form of \"use strict\".", + W098: "'{a}' is defined but never used.", + W099: "Mixed spaces and tabs.", + W100: "This character may get silently deleted by one or more browsers.", + W101: "Line is too long.", + W102: "Trailing whitespace.", + W103: "The '{a}' property is deprecated.", + W104: "'{a}' is only available in JavaScript 1.7.", + W105: "Unexpected {a} in '{b}'.", + W106: "Identifier '{a}' is not in camel case.", + W107: "Script URL.", + W108: "Strings must use doublequote.", + W109: "Strings must use singlequote.", + W110: "Mixed double and single quotes.", + W112: "Unclosed string.", + W113: "Control character in string: {a}.", + W114: "Avoid {a}.", + W115: "Octal literals are not allowed in strict mode.", + W116: "Expected '{a}' and instead saw '{b}'.", + W117: "'{a}' is not defined.", + W118: "'{a}' is only available in Mozilla JavaScript extensions (use moz option).", + W119: "'{a}' is only available in ES6 (use esnext option)." +}; + +var info = { + I001: "Comma warnings can be turned off with 'laxcomma'.", + I002: "Reserved words as properties can be used under the 'es5' option.", + I003: "ES5 option is now set per default" +}; + +exports.errors = {}; +exports.warnings = {}; +exports.info = {}; + +_.each(errors, function (desc, code) { + exports.errors[code] = { code: code, desc: desc }; +}); + +_.each(warnings, function (desc, code) { + exports.warnings[code] = { code: code, desc: desc }; +}); + +_.each(info, function (desc, code) { + exports.info[code] = { code: code, desc: desc }; +}); + +})() +},{"underscore":11}],8:[function(require,module,exports){ +var events = require('events'); + +exports.isArray = isArray; +exports.isDate = function(obj){return Object.prototype.toString.call(obj) === '[object Date]'}; +exports.isRegExp = function(obj){return Object.prototype.toString.call(obj) === '[object RegExp]'}; + + +exports.print = function () {}; +exports.puts = function () {}; +exports.debug = function() {}; + +exports.inspect = function(obj, showHidden, depth, colors) { + var seen = []; + + var stylize = function(str, styleType) { + // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics + var styles = + { 'bold' : [1, 22], + 'italic' : [3, 23], + 'underline' : [4, 24], + 'inverse' : [7, 27], + 'white' : [37, 39], + 'grey' : [90, 39], + 'black' : [30, 39], + 'blue' : [34, 39], + 'cyan' : [36, 39], + 'green' : [32, 39], + 'magenta' : [35, 39], + 'red' : [31, 39], + 'yellow' : [33, 39] }; + + var style = + { 'special': 'cyan', + 'number': 'blue', + 'boolean': 'yellow', + 'undefined': 'grey', + 'null': 'bold', + 'string': 'green', + 'date': 'magenta', + // "name": intentionally not styling + 'regexp': 'red' }[styleType]; + + if (style) { + return '\033[' + styles[style][0] + 'm' + str + + '\033[' + styles[style][1] + 'm'; + } else { + return str; + } + }; + if (! colors) { + stylize = function(str, styleType) { return str; }; + } + + function format(value, recurseTimes) { + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it + if (value && typeof value.inspect === 'function' && + // Filter out the util module, it's inspect function is special + value !== exports && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + return value.inspect(recurseTimes); + } + + // Primitive types cannot have properties + switch (typeof value) { + case 'undefined': + return stylize('undefined', 'undefined'); + + case 'string': + var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; + return stylize(simple, 'string'); + + case 'number': + return stylize('' + value, 'number'); + + case 'boolean': + return stylize('' + value, 'boolean'); + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return stylize('null', 'null'); + } + + // Look up the keys of the object. + var visible_keys = Object_keys(value); + var keys = showHidden ? Object_getOwnPropertyNames(value) : visible_keys; + + // Functions without properties can be shortcutted. + if (typeof value === 'function' && keys.length === 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + var name = value.name ? ': ' + value.name : ''; + return stylize('[Function' + name + ']', 'special'); + } + } + + // Dates without properties can be shortcutted + if (isDate(value) && keys.length === 0) { + return stylize(value.toUTCString(), 'date'); + } + + var base, type, braces; + // Determine the object type + if (isArray(value)) { + type = 'Array'; + braces = ['[', ']']; + } else { + type = 'Object'; + braces = ['{', '}']; + } + + // Make functions say that they are functions + if (typeof value === 'function') { + var n = value.name ? ': ' + value.name : ''; + base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; + } else { + base = ''; + } + + // Make dates with properties first say the date + if (isDate(value)) { + base = ' ' + value.toUTCString(); + } + + if (keys.length === 0) { + return braces[0] + base + braces[1]; + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + return stylize('[Object]', 'special'); + } + } + + seen.push(value); + + var output = keys.map(function(key) { + var name, str; + if (value.__lookupGetter__) { + if (value.__lookupGetter__(key)) { + if (value.__lookupSetter__(key)) { + str = stylize('[Getter/Setter]', 'special'); + } else { + str = stylize('[Getter]', 'special'); + } + } else { + if (value.__lookupSetter__(key)) { + str = stylize('[Setter]', 'special'); + } + } + } + if (visible_keys.indexOf(key) < 0) { + name = '[' + key + ']'; + } + if (!str) { + if (seen.indexOf(value[key]) < 0) { + if (recurseTimes === null) { + str = format(value[key]); + } else { + str = format(value[key], recurseTimes - 1); + } + if (str.indexOf('\n') > -1) { + if (isArray(value)) { + str = str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n').substr(2); + } else { + str = '\n' + str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n'); + } + } + } else { + str = stylize('[Circular]', 'special'); + } + } + if (typeof name === 'undefined') { + if (type === 'Array' && key.match(/^\d+$/)) { + return str; + } + name = JSON.stringify('' + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2); + name = stylize(name, 'name'); + } else { + name = name.replace(/'/g, "\\'") + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); + name = stylize(name, 'string'); + } + } + + return name + ': ' + str; + }); + + seen.pop(); + + var numLinesEst = 0; + var length = output.reduce(function(prev, cur) { + numLinesEst++; + if (cur.indexOf('\n') >= 0) numLinesEst++; + return prev + cur.length + 1; + }, 0); + + if (length > 50) { + output = braces[0] + + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; + + } else { + output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; + } + + return output; + } + return format(obj, (typeof depth === 'undefined' ? 2 : depth)); +}; + + +function isArray(ar) { + return ar instanceof Array || + Array.isArray(ar) || + (ar && ar !== Object.prototype && isArray(ar.__proto__)); +} + + +function isRegExp(re) { + return re instanceof RegExp || + (typeof re === 'object' && Object.prototype.toString.call(re) === '[object RegExp]'); +} + + +function isDate(d) { + if (d instanceof Date) return true; + if (typeof d !== 'object') return false; + var properties = Date.prototype && Object_getOwnPropertyNames(Date.prototype); + var proto = d.__proto__ && Object_getOwnPropertyNames(d.__proto__); + return JSON.stringify(proto) === JSON.stringify(properties); +} + +function pad(n) { + return n < 10 ? '0' + n.toString(10) : n.toString(10); +} + +var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec']; + +// 26 Feb 16:19:34 +function timestamp() { + var d = new Date(); + var time = [pad(d.getHours()), + pad(d.getMinutes()), + pad(d.getSeconds())].join(':'); + return [d.getDate(), months[d.getMonth()], time].join(' '); +} + +exports.log = function (msg) {}; + +exports.pump = null; + +var Object_keys = Object.keys || function (obj) { + var res = []; + for (var key in obj) res.push(key); + return res; +}; + +var Object_getOwnPropertyNames = Object.getOwnPropertyNames || function (obj) { + var res = []; + for (var key in obj) { + if (Object.hasOwnProperty.call(obj, key)) res.push(key); + } + return res; +}; + +var Object_create = Object.create || function (prototype, properties) { + // from es5-shim + var object; + if (prototype === null) { + object = { '__proto__' : null }; + } + else { + if (typeof prototype !== 'object') { + throw new TypeError( + 'typeof prototype[' + (typeof prototype) + '] != \'object\'' + ); + } + var Type = function () {}; + Type.prototype = prototype; + object = new Type(); + object.__proto__ = prototype; + } + if (typeof properties !== 'undefined' && Object.defineProperties) { + Object.defineProperties(object, properties); + } + return object; +}; + +exports.inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object_create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +var formatRegExp = /%[sdj%]/g; +exports.format = function(f) { + if (typeof f !== 'string') { + var objects = []; + for (var i = 0; i < arguments.length; i++) { + objects.push(exports.inspect(arguments[i])); + } + return objects.join(' '); + } + + var i = 1; + var args = arguments; + var len = args.length; + var str = String(f).replace(formatRegExp, function(x) { + if (x === '%%') return '%'; + if (i >= len) return x; + switch (x) { + case '%s': return String(args[i++]); + case '%d': return Number(args[i++]); + case '%j': return JSON.stringify(args[i++]); + default: + return x; + } + }); + for(var x = args[i]; i < len; x = args[++i]){ + if (x === null || typeof x !== 'object') { + str += ' ' + x; + } else { + str += ' ' + exports.inspect(x); + } + } + return str; +}; + +},{"events":2}],9:[function(require,module,exports){ +(function(){// UTILITY +var util = require('util'); +var Buffer = require("buffer").Buffer; +var pSlice = Array.prototype.slice; + +function objectKeys(object) { + if (Object.keys) return Object.keys(object); + var result = []; + for (var name in object) { + if (Object.prototype.hasOwnProperty.call(object, name)) { + result.push(name); + } + } + return result; +} + +// 1. The assert module provides functions that throw +// AssertionError's when particular conditions are not met. The +// assert module must conform to the following interface. + +var assert = module.exports = ok; + +// 2. The AssertionError is defined in assert. +// new assert.AssertionError({ message: message, +// actual: actual, +// expected: expected }) + +assert.AssertionError = function AssertionError(options) { + this.name = 'AssertionError'; + this.message = options.message; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + var stackStartFunction = options.stackStartFunction || fail; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, stackStartFunction); + } +}; +util.inherits(assert.AssertionError, Error); + +function replacer(key, value) { + if (value === undefined) { + return '' + value; + } + if (typeof value === 'number' && (isNaN(value) || !isFinite(value))) { + return value.toString(); + } + if (typeof value === 'function' || value instanceof RegExp) { + return value.toString(); + } + return value; +} + +function truncate(s, n) { + if (typeof s == 'string') { + return s.length < n ? s : s.slice(0, n); + } else { + return s; + } +} + +assert.AssertionError.prototype.toString = function() { + if (this.message) { + return [this.name + ':', this.message].join(' '); + } else { + return [ + this.name + ':', + truncate(JSON.stringify(this.actual, replacer), 128), + this.operator, + truncate(JSON.stringify(this.expected, replacer), 128) + ].join(' '); + } +}; + +// assert.AssertionError instanceof Error + +assert.AssertionError.__proto__ = Error.prototype; + +// At present only the three keys mentioned above are used and +// understood by the spec. Implementations or sub modules can pass +// other keys to the AssertionError's constructor - they will be +// ignored. + +// 3. All of the following functions must throw an AssertionError +// when a corresponding condition is not met, with a message that +// may be undefined if not provided. All assertion methods provide +// both the actual and expected values to the assertion error for +// display purposes. + +function fail(actual, expected, message, operator, stackStartFunction) { + throw new assert.AssertionError({ + message: message, + actual: actual, + expected: expected, + operator: operator, + stackStartFunction: stackStartFunction + }); +} + +// EXTENSION! allows for well behaved errors defined elsewhere. +assert.fail = fail; + +// 4. Pure assertion tests whether a value is truthy, as determined +// by !!guard. +// assert.ok(guard, message_opt); +// This statement is equivalent to assert.equal(true, guard, +// message_opt);. To test strictly for the value true, use +// assert.strictEqual(true, guard, message_opt);. + +function ok(value, message) { + if (!!!value) fail(value, true, message, '==', assert.ok); +} +assert.ok = ok; + +// 5. The equality assertion tests shallow, coercive equality with +// ==. +// assert.equal(actual, expected, message_opt); + +assert.equal = function equal(actual, expected, message) { + if (actual != expected) fail(actual, expected, message, '==', assert.equal); +}; + +// 6. The non-equality assertion tests for whether two objects are not equal +// with != assert.notEqual(actual, expected, message_opt); + +assert.notEqual = function notEqual(actual, expected, message) { + if (actual == expected) { + fail(actual, expected, message, '!=', assert.notEqual); + } +}; + +// 7. The equivalence assertion tests a deep equality relation. +// assert.deepEqual(actual, expected, message_opt); + +assert.deepEqual = function deepEqual(actual, expected, message) { + if (!_deepEqual(actual, expected)) { + fail(actual, expected, message, 'deepEqual', assert.deepEqual); + } +}; + +function _deepEqual(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + + } else if (Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { + if (actual.length != expected.length) return false; + + for (var i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) return false; + } + + return true; + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (actual instanceof Date && expected instanceof Date) { + return actual.getTime() === expected.getTime(); + + // 7.3. Other pairs that do not both pass typeof value == 'object', + // equivalence is determined by ==. + } else if (typeof actual != 'object' && typeof expected != 'object') { + return actual == expected; + + // 7.4. For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } +} + +function isUndefinedOrNull(value) { + return value === null || value === undefined; +} + +function isArguments(object) { + return Object.prototype.toString.call(object) == '[object Arguments]'; +} + +function objEquiv(a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) + return false; + // an identical 'prototype' property. + if (a.prototype !== b.prototype) return false; + //~~~I've managed to break Object.keys through screwy arguments passing. + // Converting to array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = pSlice.call(a); + b = pSlice.call(b); + return _deepEqual(a, b); + } + try { + var ka = objectKeys(a), + kb = objectKeys(b), + key, i; + } catch (e) {//happens when one is a string literal and the other isn't + return false; + } + // having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) + return false; + //the same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + //~~~cheap key test + for (i = ka.length - 1; i >= 0; i--) { + if (ka[i] != kb[i]) + return false; + } + //equivalent values for every corresponding key, and + //~~~possibly expensive deep test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!_deepEqual(a[key], b[key])) return false; + } + return true; +} + +// 8. The non-equivalence assertion tests for any deep inequality. +// assert.notDeepEqual(actual, expected, message_opt); + +assert.notDeepEqual = function notDeepEqual(actual, expected, message) { + if (_deepEqual(actual, expected)) { + fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); + } +}; + +// 9. The strict equality assertion tests strict equality, as determined by ===. +// assert.strictEqual(actual, expected, message_opt); + +assert.strictEqual = function strictEqual(actual, expected, message) { + if (actual !== expected) { + fail(actual, expected, message, '===', assert.strictEqual); + } +}; + +// 10. The strict non-equality assertion tests for strict inequality, as +// determined by !==. assert.notStrictEqual(actual, expected, message_opt); + +assert.notStrictEqual = function notStrictEqual(actual, expected, message) { + if (actual === expected) { + fail(actual, expected, message, '!==', assert.notStrictEqual); + } +}; + +function expectedException(actual, expected) { + if (!actual || !expected) { + return false; + } + + if (expected instanceof RegExp) { + return expected.test(actual); + } else if (actual instanceof expected) { + return true; + } else if (expected.call({}, actual) === true) { + return true; + } + + return false; +} + +function _throws(shouldThrow, block, expected, message) { + var actual; + + if (typeof expected === 'string') { + message = expected; + expected = null; + } + + try { + block(); + } catch (e) { + actual = e; + } + + message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + + (message ? ' ' + message : '.'); + + if (shouldThrow && !actual) { + fail('Missing expected exception' + message); + } + + if (!shouldThrow && expectedException(actual, expected)) { + fail('Got unwanted exception' + message); + } + + if ((shouldThrow && actual && expected && + !expectedException(actual, expected)) || (!shouldThrow && actual)) { + throw actual; + } +} + +// 11. Expected to throw an error: +// assert.throws(block, Error_opt, message_opt); + +assert.throws = function(block, /*optional*/error, /*optional*/message) { + _throws.apply(this, [true].concat(pSlice.call(arguments))); +}; + +// EXTENSION! This is annoying to write outside this module. +assert.doesNotThrow = function(block, /*optional*/error, /*optional*/message) { + _throws.apply(this, [false].concat(pSlice.call(arguments))); +}; + +assert.ifError = function(err) { if (err) {throw err;}}; + +})() +},{"util":8,"buffer":13}],11:[function(require,module,exports){ +(function(){// Underscore.js 1.4.4 +// http://underscorejs.org +// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.4.4'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + return _.filter(obj, function(value, index, list) { + return !iterator.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? null : []; + return _[first ? 'find' : 'filter'](obj, function(value) { + for (var key in attrs) { + if (attrs[key] !== value[key]) return false; + } + return true; + }); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See: https://bugs.webkit.org/show_bug.cgi?id=80797 + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity, value: -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity, value: Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array. + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + return _.isFunction(value) ? value : function(obj){ return obj[value]; }; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, value, context) { + var iterator = lookupIterator(value); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + index : index, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index < right.index ? -1 : 1; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(obj, value, context, behavior) { + var result = {}; + var iterator = lookupIterator(value || _.identity); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = function(obj, value, context) { + return group(obj, value, context, function(result, key, value) { + (_.has(result, key) ? result[key] : (result[key] = [])).push(value); + }); + }; + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = function(obj, value, context) { + return group(obj, value, context, function(result, key) { + if (!_.has(result, key)) result[key] = 0; + result[key]++; + }); + }; + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = iterator == null ? _.identity : lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n != null) && !guard) { + return slice.call(array, Math.max(array.length - n, 0)); + } else { + return array[array.length - 1]; + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + each(input, function(value) { + if (_.isArray(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Return a completely flattened version of an array. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(concat.apply(ArrayProto, arguments)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(args, "" + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, l = list.length; i < l; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, l = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < l; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + var args = slice.call(arguments, 2); + return function() { + return func.apply(context, args.concat(slice.call(arguments))); + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + var context, args, timeout, result; + var previous = 0; + var later = function() { + previous = new Date; + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) result = func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(context, args); + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func]; + push.apply(args, arguments); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + if (times <= 0) return func(); + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var values = []; + for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var pairs = []; + for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; + } + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(n); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named property is a function then invoke it; + // otherwise, return it. + _.result = function(object, property) { + if (object == null) return null; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + +}).call(this); + +})() +},{}],14:[function(require,module,exports){ +exports.readIEEE754 = function(buffer, offset, isBE, mLen, nBytes) { + var e, m, + eLen = nBytes * 8 - mLen - 1, + eMax = (1 << eLen) - 1, + eBias = eMax >> 1, + nBits = -7, + i = isBE ? 0 : (nBytes - 1), + d = isBE ? 1 : -1, + s = buffer[offset + i]; + + i += d; + + e = s & ((1 << (-nBits)) - 1); + s >>= (-nBits); + nBits += eLen; + for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8); + + m = e & ((1 << (-nBits)) - 1); + e >>= (-nBits); + nBits += mLen; + for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8); + + if (e === 0) { + e = 1 - eBias; + } else if (e === eMax) { + return m ? NaN : ((s ? -1 : 1) * Infinity); + } else { + m = m + Math.pow(2, mLen); + e = e - eBias; + } + return (s ? -1 : 1) * m * Math.pow(2, e - mLen); +}; + +exports.writeIEEE754 = function(buffer, value, offset, isBE, mLen, nBytes) { + var e, m, c, + eLen = nBytes * 8 - mLen - 1, + eMax = (1 << eLen) - 1, + eBias = eMax >> 1, + rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0), + i = isBE ? (nBytes - 1) : 0, + d = isBE ? -1 : 1, + s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0; + + value = Math.abs(value); + + if (isNaN(value) || value === Infinity) { + m = isNaN(value) ? 1 : 0; + e = eMax; + } else { + e = Math.floor(Math.log(value) / Math.LN2); + if (value * (c = Math.pow(2, -e)) < 1) { + e--; + c *= 2; + } + if (e + eBias >= 1) { + value += rt / c; + } else { + value += rt * Math.pow(2, 1 - eBias); + } + if (value * c >= 2) { + e++; + c /= 2; + } + + if (e + eBias >= eMax) { + m = 0; + e = eMax; + } else if (e + eBias >= 1) { + m = (value * c - 1) * Math.pow(2, mLen); + e = e + eBias; + } else { + m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen); + e = 0; + } + } + + for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8); + + e = (e << mLen) | m; + eLen += mLen; + for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8); + + buffer[offset + i - d] |= s * 128; +}; + +},{}],13:[function(require,module,exports){ +(function(){function SlowBuffer (size) { + this.length = size; +}; + +var assert = require('assert'); + +exports.INSPECT_MAX_BYTES = 50; + + +function toHex(n) { + if (n < 16) return '0' + n.toString(16); + return n.toString(16); +} + +function utf8ToBytes(str) { + var byteArray = []; + for (var i = 0; i < str.length; i++) + if (str.charCodeAt(i) <= 0x7F) + byteArray.push(str.charCodeAt(i)); + else { + var h = encodeURIComponent(str.charAt(i)).substr(1).split('%'); + for (var j = 0; j < h.length; j++) + byteArray.push(parseInt(h[j], 16)); + } + + return byteArray; +} + +function asciiToBytes(str) { + var byteArray = [] + for (var i = 0; i < str.length; i++ ) + // Node's code seems to be doing this and not & 0x7F.. + byteArray.push( str.charCodeAt(i) & 0xFF ); + + return byteArray; +} + +function base64ToBytes(str) { + return require("base64-js").toByteArray(str); +} + +SlowBuffer.byteLength = function (str, encoding) { + switch (encoding || "utf8") { + case 'hex': + return str.length / 2; + + case 'utf8': + case 'utf-8': + return utf8ToBytes(str).length; + + case 'ascii': + case 'binary': + return str.length; + + case 'base64': + return base64ToBytes(str).length; + + default: + throw new Error('Unknown encoding'); + } +}; + +function blitBuffer(src, dst, offset, length) { + var pos, i = 0; + while (i < length) { + if ((i+offset >= dst.length) || (i >= src.length)) + break; + + dst[i + offset] = src[i]; + i++; + } + return i; +} + +SlowBuffer.prototype.utf8Write = function (string, offset, length) { + var bytes, pos; + return SlowBuffer._charsWritten = blitBuffer(utf8ToBytes(string), this, offset, length); +}; + +SlowBuffer.prototype.asciiWrite = function (string, offset, length) { + var bytes, pos; + return SlowBuffer._charsWritten = blitBuffer(asciiToBytes(string), this, offset, length); +}; + +SlowBuffer.prototype.binaryWrite = SlowBuffer.prototype.asciiWrite; + +SlowBuffer.prototype.base64Write = function (string, offset, length) { + var bytes, pos; + return SlowBuffer._charsWritten = blitBuffer(base64ToBytes(string), this, offset, length); +}; + +SlowBuffer.prototype.base64Slice = function (start, end) { + var bytes = Array.prototype.slice.apply(this, arguments) + return require("base64-js").fromByteArray(bytes); +} + +function decodeUtf8Char(str) { + try { + return decodeURIComponent(str); + } catch (err) { + return String.fromCharCode(0xFFFD); // UTF 8 invalid char + } +} + +SlowBuffer.prototype.utf8Slice = function () { + var bytes = Array.prototype.slice.apply(this, arguments); + var res = ""; + var tmp = ""; + var i = 0; + while (i < bytes.length) { + if (bytes[i] <= 0x7F) { + res += decodeUtf8Char(tmp) + String.fromCharCode(bytes[i]); + tmp = ""; + } else + tmp += "%" + bytes[i].toString(16); + + i++; + } + + return res + decodeUtf8Char(tmp); +} + +SlowBuffer.prototype.asciiSlice = function () { + var bytes = Array.prototype.slice.apply(this, arguments); + var ret = ""; + for (var i = 0; i < bytes.length; i++) + ret += String.fromCharCode(bytes[i]); + return ret; +} + +SlowBuffer.prototype.binarySlice = SlowBuffer.prototype.asciiSlice; + +SlowBuffer.prototype.inspect = function() { + var out = [], + len = this.length; + for (var i = 0; i < len; i++) { + out[i] = toHex(this[i]); + if (i == exports.INSPECT_MAX_BYTES) { + out[i + 1] = '...'; + break; + } + } + return '<SlowBuffer ' + out.join(' ') + '>'; +}; + + +SlowBuffer.prototype.hexSlice = function(start, end) { + var len = this.length; + + if (!start || start < 0) start = 0; + if (!end || end < 0 || end > len) end = len; + + var out = ''; + for (var i = start; i < end; i++) { + out += toHex(this[i]); + } + return out; +}; + + +SlowBuffer.prototype.toString = function(encoding, start, end) { + encoding = String(encoding || 'utf8').toLowerCase(); + start = +start || 0; + if (typeof end == 'undefined') end = this.length; + + // Fastpath empty strings + if (+end == start) { + return ''; + } + + switch (encoding) { + case 'hex': + return this.hexSlice(start, end); + + case 'utf8': + case 'utf-8': + return this.utf8Slice(start, end); + + case 'ascii': + return this.asciiSlice(start, end); + + case 'binary': + return this.binarySlice(start, end); + + case 'base64': + return this.base64Slice(start, end); + + case 'ucs2': + case 'ucs-2': + return this.ucs2Slice(start, end); + + default: + throw new Error('Unknown encoding'); + } +}; + + +SlowBuffer.prototype.hexWrite = function(string, offset, length) { + offset = +offset || 0; + var remaining = this.length - offset; + if (!length) { + length = remaining; + } else { + length = +length; + if (length > remaining) { + length = remaining; + } + } + + // must be an even number of digits + var strLen = string.length; + if (strLen % 2) { + throw new Error('Invalid hex string'); + } + if (length > strLen / 2) { + length = strLen / 2; + } + for (var i = 0; i < length; i++) { + var byte = parseInt(string.substr(i * 2, 2), 16); + if (isNaN(byte)) throw new Error('Invalid hex string'); + this[offset + i] = byte; + } + SlowBuffer._charsWritten = i * 2; + return i; +}; + + +SlowBuffer.prototype.write = function(string, offset, length, encoding) { + // Support both (string, offset, length, encoding) + // and the legacy (string, encoding, offset, length) + if (isFinite(offset)) { + if (!isFinite(length)) { + encoding = length; + length = undefined; + } + } else { // legacy + var swap = encoding; + encoding = offset; + offset = length; + length = swap; + } + + offset = +offset || 0; + var remaining = this.length - offset; + if (!length) { + length = remaining; + } else { + length = +length; + if (length > remaining) { + length = remaining; + } + } + encoding = String(encoding || 'utf8').toLowerCase(); + + switch (encoding) { + case 'hex': + return this.hexWrite(string, offset, length); + + case 'utf8': + case 'utf-8': + return this.utf8Write(string, offset, length); + + case 'ascii': + return this.asciiWrite(string, offset, length); + + case 'binary': + return this.binaryWrite(string, offset, length); + + case 'base64': + return this.base64Write(string, offset, length); + + case 'ucs2': + case 'ucs-2': + return this.ucs2Write(string, offset, length); + + default: + throw new Error('Unknown encoding'); + } +}; + + +// slice(start, end) +SlowBuffer.prototype.slice = function(start, end) { + if (end === undefined) end = this.length; + + if (end > this.length) { + throw new Error('oob'); + } + if (start > end) { + throw new Error('oob'); + } + + return new Buffer(this, end - start, +start); +}; + +SlowBuffer.prototype.copy = function(target, targetstart, sourcestart, sourceend) { + var temp = []; + for (var i=sourcestart; i<sourceend; i++) { + assert.ok(typeof this[i] !== 'undefined', "copying undefined buffer bytes!"); + temp.push(this[i]); + } + + for (var i=targetstart; i<targetstart+temp.length; i++) { + target[i] = temp[i-targetstart]; + } +}; + +SlowBuffer.prototype.fill = function(value, start, end) { + if (end > this.length) { + throw new Error('oob'); + } + if (start > end) { + throw new Error('oob'); + } + + for (var i = start; i < end; i++) { + this[i] = value; + } +} + +function coerce(length) { + // Coerce length to a number (possibly NaN), round up + // in case it's fractional (e.g. 123.456) then do a + // double negate to coerce a NaN to 0. Easy, right? + length = ~~Math.ceil(+length); + return length < 0 ? 0 : length; +} + + +// Buffer + +function Buffer(subject, encoding, offset) { + if (!(this instanceof Buffer)) { + return new Buffer(subject, encoding, offset); + } + + var type; + + // Are we slicing? + if (typeof offset === 'number') { + this.length = coerce(encoding); + this.parent = subject; + this.offset = offset; + } else { + // Find the length + switch (type = typeof subject) { + case 'number': + this.length = coerce(subject); + break; + + case 'string': + this.length = Buffer.byteLength(subject, encoding); + break; + + case 'object': // Assume object is an array + this.length = coerce(subject.length); + break; + + default: + throw new Error('First argument needs to be a number, ' + + 'array or string.'); + } + + if (this.length > Buffer.poolSize) { + // Big buffer, just alloc one. + this.parent = new SlowBuffer(this.length); + this.offset = 0; + + } else { + // Small buffer. + if (!pool || pool.length - pool.used < this.length) allocPool(); + this.parent = pool; + this.offset = pool.used; + pool.used += this.length; + } + + // Treat array-ish objects as a byte array. + if (isArrayIsh(subject)) { + for (var i = 0; i < this.length; i++) { + if (subject instanceof Buffer) { + this.parent[i + this.offset] = subject.readUInt8(i); + } + else { + this.parent[i + this.offset] = subject[i]; + } + } + } else if (type == 'string') { + // We are a string + this.length = this.write(subject, 0, encoding); + } + } + +} + +function isArrayIsh(subject) { + return Array.isArray(subject) || Buffer.isBuffer(subject) || + subject && typeof subject === 'object' && + typeof subject.length === 'number'; +} + +exports.SlowBuffer = SlowBuffer; +exports.Buffer = Buffer; + +Buffer.poolSize = 8 * 1024; +var pool; + +function allocPool() { + pool = new SlowBuffer(Buffer.poolSize); + pool.used = 0; +} + + +// Static methods +Buffer.isBuffer = function isBuffer(b) { + return b instanceof Buffer || b instanceof SlowBuffer; +}; + +Buffer.concat = function (list, totalLength) { + if (!Array.isArray(list)) { + throw new Error("Usage: Buffer.concat(list, [totalLength])\n \ + list should be an Array."); + } + + if (list.length === 0) { + return new Buffer(0); + } else if (list.length === 1) { + return list[0]; + } + + if (typeof totalLength !== 'number') { + totalLength = 0; + for (var i = 0; i < list.length; i++) { + var buf = list[i]; + totalLength += buf.length; + } + } + + var buffer = new Buffer(totalLength); + var pos = 0; + for (var i = 0; i < list.length; i++) { + var buf = list[i]; + buf.copy(buffer, pos); + pos += buf.length; + } + return buffer; +}; + +// Inspect +Buffer.prototype.inspect = function inspect() { + var out = [], + len = this.length; + + for (var i = 0; i < len; i++) { + out[i] = toHex(this.parent[i + this.offset]); + if (i == exports.INSPECT_MAX_BYTES) { + out[i + 1] = '...'; + break; + } + } + + return '<Buffer ' + out.join(' ') + '>'; +}; + + +Buffer.prototype.get = function get(i) { + if (i < 0 || i >= this.length) throw new Error('oob'); + return this.parent[this.offset + i]; +}; + + +Buffer.prototype.set = function set(i, v) { + if (i < 0 || i >= this.length) throw new Error('oob'); + return this.parent[this.offset + i] = v; +}; + + +// write(string, offset = 0, length = buffer.length-offset, encoding = 'utf8') +Buffer.prototype.write = function(string, offset, length, encoding) { + // Support both (string, offset, length, encoding) + // and the legacy (string, encoding, offset, length) + if (isFinite(offset)) { + if (!isFinite(length)) { + encoding = length; + length = undefined; + } + } else { // legacy + var swap = encoding; + encoding = offset; + offset = length; + length = swap; + } + + offset = +offset || 0; + var remaining = this.length - offset; + if (!length) { + length = remaining; + } else { + length = +length; + if (length > remaining) { + length = remaining; + } + } + encoding = String(encoding || 'utf8').toLowerCase(); + + var ret; + switch (encoding) { + case 'hex': + ret = this.parent.hexWrite(string, this.offset + offset, length); + break; + + case 'utf8': + case 'utf-8': + ret = this.parent.utf8Write(string, this.offset + offset, length); + break; + + case 'ascii': + ret = this.parent.asciiWrite(string, this.offset + offset, length); + break; + + case 'binary': + ret = this.parent.binaryWrite(string, this.offset + offset, length); + break; + + case 'base64': + // Warning: maxLength not taken into account in base64Write + ret = this.parent.base64Write(string, this.offset + offset, length); + break; + + case 'ucs2': + case 'ucs-2': + ret = this.parent.ucs2Write(string, this.offset + offset, length); + break; + + default: + throw new Error('Unknown encoding'); + } + + Buffer._charsWritten = SlowBuffer._charsWritten; + + return ret; +}; + + +// toString(encoding, start=0, end=buffer.length) +Buffer.prototype.toString = function(encoding, start, end) { + encoding = String(encoding || 'utf8').toLowerCase(); + + if (typeof start == 'undefined' || start < 0) { + start = 0; + } else if (start > this.length) { + start = this.length; + } + + if (typeof end == 'undefined' || end > this.length) { + end = this.length; + } else if (end < 0) { + end = 0; + } + + start = start + this.offset; + end = end + this.offset; + + switch (encoding) { + case 'hex': + return this.parent.hexSlice(start, end); + + case 'utf8': + case 'utf-8': + return this.parent.utf8Slice(start, end); + + case 'ascii': + return this.parent.asciiSlice(start, end); + + case 'binary': + return this.parent.binarySlice(start, end); + + case 'base64': + return this.parent.base64Slice(start, end); + + case 'ucs2': + case 'ucs-2': + return this.parent.ucs2Slice(start, end); + + default: + throw new Error('Unknown encoding'); + } +}; + + +// byteLength +Buffer.byteLength = SlowBuffer.byteLength; + + +// fill(value, start=0, end=buffer.length) +Buffer.prototype.fill = function fill(value, start, end) { + value || (value = 0); + start || (start = 0); + end || (end = this.length); + + if (typeof value === 'string') { + value = value.charCodeAt(0); + } + if (!(typeof value === 'number') || isNaN(value)) { + throw new Error('value is not a number'); + } + + if (end < start) throw new Error('end < start'); + + // Fill 0 bytes; we're done + if (end === start) return 0; + if (this.length == 0) return 0; + + if (start < 0 || start >= this.length) { + throw new Error('start out of bounds'); + } + + if (end < 0 || end > this.length) { + throw new Error('end out of bounds'); + } + + return this.parent.fill(value, + start + this.offset, + end + this.offset); +}; + + +// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) +Buffer.prototype.copy = function(target, target_start, start, end) { + var source = this; + start || (start = 0); + end || (end = this.length); + target_start || (target_start = 0); + + if (end < start) throw new Error('sourceEnd < sourceStart'); + + // Copy 0 bytes; we're done + if (end === start) return 0; + if (target.length == 0 || source.length == 0) return 0; + + if (target_start < 0 || target_start >= target.length) { + throw new Error('targetStart out of bounds'); + } + + if (start < 0 || start >= source.length) { + throw new Error('sourceStart out of bounds'); + } + + if (end < 0 || end > source.length) { + throw new Error('sourceEnd out of bounds'); + } + + // Are we oob? + if (end > this.length) { + end = this.length; + } + + if (target.length - target_start < end - start) { + end = target.length - target_start + start; + } + + return this.parent.copy(target.parent, + target_start + target.offset, + start + this.offset, + end + this.offset); +}; + + +// slice(start, end) +Buffer.prototype.slice = function(start, end) { + if (end === undefined) end = this.length; + if (end > this.length) throw new Error('oob'); + if (start > end) throw new Error('oob'); + + return new Buffer(this.parent, end - start, +start + this.offset); +}; + + +// Legacy methods for backwards compatibility. + +Buffer.prototype.utf8Slice = function(start, end) { + return this.toString('utf8', start, end); +}; + +Buffer.prototype.binarySlice = function(start, end) { + return this.toString('binary', start, end); +}; + +Buffer.prototype.asciiSlice = function(start, end) { + return this.toString('ascii', start, end); +}; + +Buffer.prototype.utf8Write = function(string, offset) { + return this.write(string, offset, 'utf8'); +}; + +Buffer.prototype.binaryWrite = function(string, offset) { + return this.write(string, offset, 'binary'); +}; + +Buffer.prototype.asciiWrite = function(string, offset) { + return this.write(string, offset, 'ascii'); +}; + +Buffer.prototype.readUInt8 = function(offset, noAssert) { + var buffer = this; + + if (!noAssert) { + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset < buffer.length, + 'Trying to read beyond buffer length'); + } + + if (offset >= buffer.length) return; + + return buffer.parent[buffer.offset + offset]; +}; + +function readUInt16(buffer, offset, isBigEndian, noAssert) { + var val = 0; + + + if (!noAssert) { + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 1 < buffer.length, + 'Trying to read beyond buffer length'); + } + + if (offset >= buffer.length) return 0; + + if (isBigEndian) { + val = buffer.parent[buffer.offset + offset] << 8; + if (offset + 1 < buffer.length) { + val |= buffer.parent[buffer.offset + offset + 1]; + } + } else { + val = buffer.parent[buffer.offset + offset]; + if (offset + 1 < buffer.length) { + val |= buffer.parent[buffer.offset + offset + 1] << 8; + } + } + + return val; +} + +Buffer.prototype.readUInt16LE = function(offset, noAssert) { + return readUInt16(this, offset, false, noAssert); +}; + +Buffer.prototype.readUInt16BE = function(offset, noAssert) { + return readUInt16(this, offset, true, noAssert); +}; + +function readUInt32(buffer, offset, isBigEndian, noAssert) { + var val = 0; + + if (!noAssert) { + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 3 < buffer.length, + 'Trying to read beyond buffer length'); + } + + if (offset >= buffer.length) return 0; + + if (isBigEndian) { + if (offset + 1 < buffer.length) + val = buffer.parent[buffer.offset + offset + 1] << 16; + if (offset + 2 < buffer.length) + val |= buffer.parent[buffer.offset + offset + 2] << 8; + if (offset + 3 < buffer.length) + val |= buffer.parent[buffer.offset + offset + 3]; + val = val + (buffer.parent[buffer.offset + offset] << 24 >>> 0); + } else { + if (offset + 2 < buffer.length) + val = buffer.parent[buffer.offset + offset + 2] << 16; + if (offset + 1 < buffer.length) + val |= buffer.parent[buffer.offset + offset + 1] << 8; + val |= buffer.parent[buffer.offset + offset]; + if (offset + 3 < buffer.length) + val = val + (buffer.parent[buffer.offset + offset + 3] << 24 >>> 0); + } + + return val; +} + +Buffer.prototype.readUInt32LE = function(offset, noAssert) { + return readUInt32(this, offset, false, noAssert); +}; + +Buffer.prototype.readUInt32BE = function(offset, noAssert) { + return readUInt32(this, offset, true, noAssert); +}; + + +/* + * Signed integer types, yay team! A reminder on how two's complement actually + * works. The first bit is the signed bit, i.e. tells us whether or not the + * number should be positive or negative. If the two's complement value is + * positive, then we're done, as it's equivalent to the unsigned representation. + * + * Now if the number is positive, you're pretty much done, you can just leverage + * the unsigned translations and return those. Unfortunately, negative numbers + * aren't quite that straightforward. + * + * At first glance, one might be inclined to use the traditional formula to + * translate binary numbers between the positive and negative values in two's + * complement. (Though it doesn't quite work for the most negative value) + * Mainly: + * - invert all the bits + * - add one to the result + * + * Of course, this doesn't quite work in Javascript. Take for example the value + * of -128. This could be represented in 16 bits (big-endian) as 0xff80. But of + * course, Javascript will do the following: + * + * > ~0xff80 + * -65409 + * + * Whoh there, Javascript, that's not quite right. But wait, according to + * Javascript that's perfectly correct. When Javascript ends up seeing the + * constant 0xff80, it has no notion that it is actually a signed number. It + * assumes that we've input the unsigned value 0xff80. Thus, when it does the + * binary negation, it casts it into a signed value, (positive 0xff80). Then + * when you perform binary negation on that, it turns it into a negative number. + * + * Instead, we're going to have to use the following general formula, that works + * in a rather Javascript friendly way. I'm glad we don't support this kind of + * weird numbering scheme in the kernel. + * + * (BIT-MAX - (unsigned)val + 1) * -1 + * + * The astute observer, may think that this doesn't make sense for 8-bit numbers + * (really it isn't necessary for them). However, when you get 16-bit numbers, + * you do. Let's go back to our prior example and see how this will look: + * + * (0xffff - 0xff80 + 1) * -1 + * (0x007f + 1) * -1 + * (0x0080) * -1 + */ +Buffer.prototype.readInt8 = function(offset, noAssert) { + var buffer = this; + var neg; + + if (!noAssert) { + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset < buffer.length, + 'Trying to read beyond buffer length'); + } + + if (offset >= buffer.length) return; + + neg = buffer.parent[buffer.offset + offset] & 0x80; + if (!neg) { + return (buffer.parent[buffer.offset + offset]); + } + + return ((0xff - buffer.parent[buffer.offset + offset] + 1) * -1); +}; + +function readInt16(buffer, offset, isBigEndian, noAssert) { + var neg, val; + + if (!noAssert) { + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 1 < buffer.length, + 'Trying to read beyond buffer length'); + } + + val = readUInt16(buffer, offset, isBigEndian, noAssert); + neg = val & 0x8000; + if (!neg) { + return val; + } + + return (0xffff - val + 1) * -1; +} + +Buffer.prototype.readInt16LE = function(offset, noAssert) { + return readInt16(this, offset, false, noAssert); +}; + +Buffer.prototype.readInt16BE = function(offset, noAssert) { + return readInt16(this, offset, true, noAssert); +}; + +function readInt32(buffer, offset, isBigEndian, noAssert) { + var neg, val; + + if (!noAssert) { + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 3 < buffer.length, + 'Trying to read beyond buffer length'); + } + + val = readUInt32(buffer, offset, isBigEndian, noAssert); + neg = val & 0x80000000; + if (!neg) { + return (val); + } + + return (0xffffffff - val + 1) * -1; +} + +Buffer.prototype.readInt32LE = function(offset, noAssert) { + return readInt32(this, offset, false, noAssert); +}; + +Buffer.prototype.readInt32BE = function(offset, noAssert) { + return readInt32(this, offset, true, noAssert); +}; + +function readFloat(buffer, offset, isBigEndian, noAssert) { + if (!noAssert) { + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset + 3 < buffer.length, + 'Trying to read beyond buffer length'); + } + + return require('./buffer_ieee754').readIEEE754(buffer, offset, isBigEndian, + 23, 4); +} + +Buffer.prototype.readFloatLE = function(offset, noAssert) { + return readFloat(this, offset, false, noAssert); +}; + +Buffer.prototype.readFloatBE = function(offset, noAssert) { + return readFloat(this, offset, true, noAssert); +}; + +function readDouble(buffer, offset, isBigEndian, noAssert) { + if (!noAssert) { + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset + 7 < buffer.length, + 'Trying to read beyond buffer length'); + } + + return require('./buffer_ieee754').readIEEE754(buffer, offset, isBigEndian, + 52, 8); +} + +Buffer.prototype.readDoubleLE = function(offset, noAssert) { + return readDouble(this, offset, false, noAssert); +}; + +Buffer.prototype.readDoubleBE = function(offset, noAssert) { + return readDouble(this, offset, true, noAssert); +}; + + +/* + * We have to make sure that the value is a valid integer. This means that it is + * non-negative. It has no fractional component and that it does not exceed the + * maximum allowed value. + * + * value The number to check for validity + * + * max The maximum value + */ +function verifuint(value, max) { + assert.ok(typeof (value) == 'number', + 'cannot write a non-number as a number'); + + assert.ok(value >= 0, + 'specified a negative value for writing an unsigned value'); + + assert.ok(value <= max, 'value is larger than maximum value for type'); + + assert.ok(Math.floor(value) === value, 'value has a fractional component'); +} + +Buffer.prototype.writeUInt8 = function(value, offset, noAssert) { + var buffer = this; + + if (!noAssert) { + assert.ok(value !== undefined && value !== null, + 'missing value'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset < buffer.length, + 'trying to write beyond buffer length'); + + verifuint(value, 0xff); + } + + if (offset < buffer.length) { + buffer.parent[buffer.offset + offset] = value; + } +}; + +function writeUInt16(buffer, value, offset, isBigEndian, noAssert) { + if (!noAssert) { + assert.ok(value !== undefined && value !== null, + 'missing value'); + + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 1 < buffer.length, + 'trying to write beyond buffer length'); + + verifuint(value, 0xffff); + } + + for (var i = 0; i < Math.min(buffer.length - offset, 2); i++) { + buffer.parent[buffer.offset + offset + i] = + (value & (0xff << (8 * (isBigEndian ? 1 - i : i)))) >>> + (isBigEndian ? 1 - i : i) * 8; + } + +} + +Buffer.prototype.writeUInt16LE = function(value, offset, noAssert) { + writeUInt16(this, value, offset, false, noAssert); +}; + +Buffer.prototype.writeUInt16BE = function(value, offset, noAssert) { + writeUInt16(this, value, offset, true, noAssert); +}; + +function writeUInt32(buffer, value, offset, isBigEndian, noAssert) { + if (!noAssert) { + assert.ok(value !== undefined && value !== null, + 'missing value'); + + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 3 < buffer.length, + 'trying to write beyond buffer length'); + + verifuint(value, 0xffffffff); + } + + for (var i = 0; i < Math.min(buffer.length - offset, 4); i++) { + buffer.parent[buffer.offset + offset + i] = + (value >>> (isBigEndian ? 3 - i : i) * 8) & 0xff; + } +} + +Buffer.prototype.writeUInt32LE = function(value, offset, noAssert) { + writeUInt32(this, value, offset, false, noAssert); +}; + +Buffer.prototype.writeUInt32BE = function(value, offset, noAssert) { + writeUInt32(this, value, offset, true, noAssert); +}; + + +/* + * We now move onto our friends in the signed number category. Unlike unsigned + * numbers, we're going to have to worry a bit more about how we put values into + * arrays. Since we are only worrying about signed 32-bit values, we're in + * slightly better shape. Unfortunately, we really can't do our favorite binary + * & in this system. It really seems to do the wrong thing. For example: + * + * > -32 & 0xff + * 224 + * + * What's happening above is really: 0xe0 & 0xff = 0xe0. However, the results of + * this aren't treated as a signed number. Ultimately a bad thing. + * + * What we're going to want to do is basically create the unsigned equivalent of + * our representation and pass that off to the wuint* functions. To do that + * we're going to do the following: + * + * - if the value is positive + * we can pass it directly off to the equivalent wuint + * - if the value is negative + * we do the following computation: + * mb + val + 1, where + * mb is the maximum unsigned value in that byte size + * val is the Javascript negative integer + * + * + * As a concrete value, take -128. In signed 16 bits this would be 0xff80. If + * you do out the computations: + * + * 0xffff - 128 + 1 + * 0xffff - 127 + * 0xff80 + * + * You can then encode this value as the signed version. This is really rather + * hacky, but it should work and get the job done which is our goal here. + */ + +/* + * A series of checks to make sure we actually have a signed 32-bit number + */ +function verifsint(value, max, min) { + assert.ok(typeof (value) == 'number', + 'cannot write a non-number as a number'); + + assert.ok(value <= max, 'value larger than maximum allowed value'); + + assert.ok(value >= min, 'value smaller than minimum allowed value'); + + assert.ok(Math.floor(value) === value, 'value has a fractional component'); +} + +function verifIEEE754(value, max, min) { + assert.ok(typeof (value) == 'number', + 'cannot write a non-number as a number'); + + assert.ok(value <= max, 'value larger than maximum allowed value'); + + assert.ok(value >= min, 'value smaller than minimum allowed value'); +} + +Buffer.prototype.writeInt8 = function(value, offset, noAssert) { + var buffer = this; + + if (!noAssert) { + assert.ok(value !== undefined && value !== null, + 'missing value'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset < buffer.length, + 'Trying to write beyond buffer length'); + + verifsint(value, 0x7f, -0x80); + } + + if (value >= 0) { + buffer.writeUInt8(value, offset, noAssert); + } else { + buffer.writeUInt8(0xff + value + 1, offset, noAssert); + } +}; + +function writeInt16(buffer, value, offset, isBigEndian, noAssert) { + if (!noAssert) { + assert.ok(value !== undefined && value !== null, + 'missing value'); + + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 1 < buffer.length, + 'Trying to write beyond buffer length'); + + verifsint(value, 0x7fff, -0x8000); + } + + if (value >= 0) { + writeUInt16(buffer, value, offset, isBigEndian, noAssert); + } else { + writeUInt16(buffer, 0xffff + value + 1, offset, isBigEndian, noAssert); + } +} + +Buffer.prototype.writeInt16LE = function(value, offset, noAssert) { + writeInt16(this, value, offset, false, noAssert); +}; + +Buffer.prototype.writeInt16BE = function(value, offset, noAssert) { + writeInt16(this, value, offset, true, noAssert); +}; + +function writeInt32(buffer, value, offset, isBigEndian, noAssert) { + if (!noAssert) { + assert.ok(value !== undefined && value !== null, + 'missing value'); + + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 3 < buffer.length, + 'Trying to write beyond buffer length'); + + verifsint(value, 0x7fffffff, -0x80000000); + } + + if (value >= 0) { + writeUInt32(buffer, value, offset, isBigEndian, noAssert); + } else { + writeUInt32(buffer, 0xffffffff + value + 1, offset, isBigEndian, noAssert); + } +} + +Buffer.prototype.writeInt32LE = function(value, offset, noAssert) { + writeInt32(this, value, offset, false, noAssert); +}; + +Buffer.prototype.writeInt32BE = function(value, offset, noAssert) { + writeInt32(this, value, offset, true, noAssert); +}; + +function writeFloat(buffer, value, offset, isBigEndian, noAssert) { + if (!noAssert) { + assert.ok(value !== undefined && value !== null, + 'missing value'); + + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 3 < buffer.length, + 'Trying to write beyond buffer length'); + + verifIEEE754(value, 3.4028234663852886e+38, -3.4028234663852886e+38); + } + + require('./buffer_ieee754').writeIEEE754(buffer, value, offset, isBigEndian, + 23, 4); +} + +Buffer.prototype.writeFloatLE = function(value, offset, noAssert) { + writeFloat(this, value, offset, false, noAssert); +}; + +Buffer.prototype.writeFloatBE = function(value, offset, noAssert) { + writeFloat(this, value, offset, true, noAssert); +}; + +function writeDouble(buffer, value, offset, isBigEndian, noAssert) { + if (!noAssert) { + assert.ok(value !== undefined && value !== null, + 'missing value'); + + assert.ok(typeof (isBigEndian) === 'boolean', + 'missing or invalid endian'); + + assert.ok(offset !== undefined && offset !== null, + 'missing offset'); + + assert.ok(offset + 7 < buffer.length, + 'Trying to write beyond buffer length'); + + verifIEEE754(value, 1.7976931348623157E+308, -1.7976931348623157E+308); + } + + require('./buffer_ieee754').writeIEEE754(buffer, value, offset, isBigEndian, + 52, 8); +} + +Buffer.prototype.writeDoubleLE = function(value, offset, noAssert) { + writeDouble(this, value, offset, false, noAssert); +}; + +Buffer.prototype.writeDoubleBE = function(value, offset, noAssert) { + writeDouble(this, value, offset, true, noAssert); +}; + +SlowBuffer.prototype.readUInt8 = Buffer.prototype.readUInt8; +SlowBuffer.prototype.readUInt16LE = Buffer.prototype.readUInt16LE; +SlowBuffer.prototype.readUInt16BE = Buffer.prototype.readUInt16BE; +SlowBuffer.prototype.readUInt32LE = Buffer.prototype.readUInt32LE; +SlowBuffer.prototype.readUInt32BE = Buffer.prototype.readUInt32BE; +SlowBuffer.prototype.readInt8 = Buffer.prototype.readInt8; +SlowBuffer.prototype.readInt16LE = Buffer.prototype.readInt16LE; +SlowBuffer.prototype.readInt16BE = Buffer.prototype.readInt16BE; +SlowBuffer.prototype.readInt32LE = Buffer.prototype.readInt32LE; +SlowBuffer.prototype.readInt32BE = Buffer.prototype.readInt32BE; +SlowBuffer.prototype.readFloatLE = Buffer.prototype.readFloatLE; +SlowBuffer.prototype.readFloatBE = Buffer.prototype.readFloatBE; +SlowBuffer.prototype.readDoubleLE = Buffer.prototype.readDoubleLE; +SlowBuffer.prototype.readDoubleBE = Buffer.prototype.readDoubleBE; +SlowBuffer.prototype.writeUInt8 = Buffer.prototype.writeUInt8; +SlowBuffer.prototype.writeUInt16LE = Buffer.prototype.writeUInt16LE; +SlowBuffer.prototype.writeUInt16BE = Buffer.prototype.writeUInt16BE; +SlowBuffer.prototype.writeUInt32LE = Buffer.prototype.writeUInt32LE; +SlowBuffer.prototype.writeUInt32BE = Buffer.prototype.writeUInt32BE; +SlowBuffer.prototype.writeInt8 = Buffer.prototype.writeInt8; +SlowBuffer.prototype.writeInt16LE = Buffer.prototype.writeInt16LE; +SlowBuffer.prototype.writeInt16BE = Buffer.prototype.writeInt16BE; +SlowBuffer.prototype.writeInt32LE = Buffer.prototype.writeInt32LE; +SlowBuffer.prototype.writeInt32BE = Buffer.prototype.writeInt32BE; +SlowBuffer.prototype.writeFloatLE = Buffer.prototype.writeFloatLE; +SlowBuffer.prototype.writeFloatBE = Buffer.prototype.writeFloatBE; +SlowBuffer.prototype.writeDoubleLE = Buffer.prototype.writeDoubleLE; +SlowBuffer.prototype.writeDoubleBE = Buffer.prototype.writeDoubleBE; + +})() +},{"assert":9,"./buffer_ieee754":14,"base64-js":15}],15:[function(require,module,exports){ +(function (exports) { + 'use strict'; + + var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + + function b64ToByteArray(b64) { + var i, j, l, tmp, placeHolders, arr; + + if (b64.length % 4 > 0) { + throw 'Invalid string. Length must be a multiple of 4'; + } + + // the number of equal signs (place holders) + // if there are two placeholders, than the two characters before it + // represent one byte + // if there is only one, then the three characters before it represent 2 bytes + // this is just a cheap hack to not do indexOf twice + placeHolders = b64.indexOf('='); + placeHolders = placeHolders > 0 ? b64.length - placeHolders : 0; + + // base64 is 4/3 + up to two characters of the original data + arr = [];//new Uint8Array(b64.length * 3 / 4 - placeHolders); + + // if there are placeholders, only get up to the last complete 4 chars + l = placeHolders > 0 ? b64.length - 4 : b64.length; + + for (i = 0, j = 0; i < l; i += 4, j += 3) { + tmp = (lookup.indexOf(b64[i]) << 18) | (lookup.indexOf(b64[i + 1]) << 12) | (lookup.indexOf(b64[i + 2]) << 6) | lookup.indexOf(b64[i + 3]); + arr.push((tmp & 0xFF0000) >> 16); + arr.push((tmp & 0xFF00) >> 8); + arr.push(tmp & 0xFF); + } + + if (placeHolders === 2) { + tmp = (lookup.indexOf(b64[i]) << 2) | (lookup.indexOf(b64[i + 1]) >> 4); + arr.push(tmp & 0xFF); + } else if (placeHolders === 1) { + tmp = (lookup.indexOf(b64[i]) << 10) | (lookup.indexOf(b64[i + 1]) << 4) | (lookup.indexOf(b64[i + 2]) >> 2); + arr.push((tmp >> 8) & 0xFF); + arr.push(tmp & 0xFF); + } + + return arr; + } + + function uint8ToBase64(uint8) { + var i, + extraBytes = uint8.length % 3, // if we have 1 byte left, pad 2 bytes + output = "", + temp, length; + + function tripletToBase64 (num) { + return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F]; + }; + + // go through the array every three bytes, we'll deal with trailing stuff later + for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) { + temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2]); + output += tripletToBase64(temp); + } + + // pad the end with zeros, but make sure to not forget the extra bytes + switch (extraBytes) { + case 1: + temp = uint8[uint8.length - 1]; + output += lookup[temp >> 2]; + output += lookup[(temp << 4) & 0x3F]; + output += '=='; + break; + case 2: + temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1]); + output += lookup[temp >> 10]; + output += lookup[(temp >> 4) & 0x3F]; + output += lookup[(temp << 2) & 0x3F]; + output += '='; + break; + } + + return output; + } + + module.exports.toByteArray = b64ToByteArray; + module.exports.fromByteArray = uint8ToBase64; +}()); + +},{}]},{},["E/GbHF"]) +; +JSHINT = require('jshint').JSHINT; +}()); diff --git a/dom/system/gonk/tests/marionette/ril_jshint/jshintrc b/dom/system/gonk/tests/marionette/ril_jshint/jshintrc new file mode 100644 index 000000000..437fe1a6f --- /dev/null +++ b/dom/system/gonk/tests/marionette/ril_jshint/jshintrc @@ -0,0 +1,118 @@ +{ + // JSHint Default Configuration File (as on JSHint website) + // See http://jshint.com/docs/ for more details + + // Modify for RIL usage. + + "maxerr" : 10000, // {int} Maximum error before stopping + + // Enforcing + "bitwise" : false, // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase" : false, // true: Identifiers must be in camelCase + "curly" : false, // true: Require {} for every new block or scope + "eqeqeq" : false, // true: Require triple equals (===) for comparison + "forin" : false, // true: Require filtering for..in loops with obj.hasOwnProperty() + "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` + //"indent" : 2, // {int} Number of spaces to use for indentation + "latedef" : false, // true: Require variables/functions to be defined before being used + "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty" : false, // true: Prohibit use of empty blocks + "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus" : false, // true: Prohibit use of `++` & `--` + "quotmark" : false, // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef" : false, // true: Require all non-global variables to be declared (prevents global leaks) + "unused" : false, // true: Require all defined variables be used + "strict" : false, // true: Requires all functions run in ES5 Strict Mode + "trailing" : false, // true: Prohibit trailing whitespaces + "maxparams" : false, // {int} Max number of formal params allowed per function + "maxdepth" : false, // {int} Max depth of nested blocks (within functions) + "maxstatements" : false, // {int} Max number statements per function + "maxcomplexity" : false, // {int} Max cyclomatic complexity per function + "maxlen" : false, // {int} Max number of characters per line + + // Relaxing + "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss" : false, // true: Tolerate assignments where comparisons would be expected + "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. + "eqnull" : true, // true: Tolerate use of `== null` + "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) + "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) + "moz" : true, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + // (ex: `for each`, multiple try/catch, function expression…) + "evil" : false, // true: Tolerate use of `eval` and `new Function()` + "expr" : false, // true: Tolerate `ExpressionStatement` as Programs + "funcscope" : false, // true: Tolerate defining variables inside control statements" + "globalstrict" : true, // true: Allow global "use strict" (also enables 'strict') + "iterator" : false, // true: Tolerate using the `__iterator__` property + "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak" : true, // true: Tolerate possibly unsafe line breakings + "laxcomma" : false, // true: Tolerate comma-first style coding + "loopfunc" : false, // true: Tolerate functions being defined in loops + "multistr" : false, // true: Tolerate multi-line strings + "proto" : true, // true: Tolerate using the `__proto__` property + "scripturl" : false, // true: Tolerate script-targeted URLs + "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment + "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` + "validthis" : true, // true: Tolerate using this in a non-constructor function + + // Environments + "browser" : false, // Web Browser (window, document, etc) + "couch" : false, // CouchDB + "devel" : true, // Development/debugging (alert, confirm, etc) + "dojo" : false, // Dojo Toolkit + "jquery" : false, // jQuery + "mootools" : false, // MooTools + "node" : false, // Node.js + "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) + "prototypejs" : false, // Prototype and Scriptaculous + "rhino" : false, // Rhino + "worker" : true, // Web Workers + "wsh" : false, // Windows Scripting Host + "yui" : false, // Yahoo User Interface + + // Legacy + "nomen" : false, // true: Prohibit dangling `_` in variables + "onevar" : false, // true: Allow only one `var` statement per function + "passfail" : false, // true: Stop on first error + "white" : false, // true: Check against strict whitespace and indentation rules + + // Custom Globals + "predef" : [ ], // additional predefined global variables + + "globals": { + "ChromeWorker": false, + "Components": false, + "DOMRequestIpcHelper": false, + "ObjectWrapper": false, + "PhoneNumberUtils": false, + "RILNetworkInterface": false, + "Services": false, + "Uint8Array": false, + "WAP": false, + "XPCOMUtils": false, + "cpmm": false, + "dump": false, + "gAudioManager": false, + "gMessageManager": false, + "gMobileMessageDatabaseService": false, + "gMobileMessageService": false, + "gNetworkManager": false, + "gPowerManagerService": false, + "gSettingsService": false, + "gSmsService": false, + "gSystemMessenger": false, + "gSystemWorkerManager": false, + "gTimeService": false, + "gUUIDGenerator": false, + "ppmm": true, + + "__end_guardian_for_easy_sorting__": false + } +} diff --git a/dom/system/gonk/tests/marionette/test_all_network_info.js b/dom/system/gonk/tests/marionette/test_all_network_info.js new file mode 100644 index 000000000..5225ab6d6 --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_all_network_info.js @@ -0,0 +1,106 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = "head.js"; + +var networkManager = + Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager); +ok(networkManager, + "networkManager.constructor is " + networkManager.constructor); + +var wifiManager = window.navigator.mozWifiManager; +ok(wifiManager, "wifiManager.constructor is " + wifiManager.constructor); + +function setEmulatorAPN() { + let apn = [ + [{"carrier":"T-Mobile US", + "apn":"epc.tmobile.com", + "mmsc":"http://mms.msg.eng.t-mobile.com/mms/wapenc", + "types":["default","supl","mms","ims","dun", "fota"]}] + ]; + + return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, apn); +} + +function ensureWifiEnabled(aEnabled) { + if (wifiManager.enabled === aEnabled) { + log('Already ' + (aEnabled ? 'enabled' : 'disabled')); + return Promise.resolve(); + } + return requestWifiEnabled(aEnabled); +} + +function requestWifiEnabled(aEnabled) { + let promises = []; + + promises.push(waitForTargetEvent(wifiManager, aEnabled ? 'enabled' : 'disabled', + function() { + return wifiManager.enabled === aEnabled ? true : false; + })); + promises.push(setSettings(SETTINGS_KEY_WIFI_ENABLED, aEnabled)); + + return Promise.all(promises); +} + +// Test initial State +function verifyInitialState() { + log("= verifyInitialState ="); + + // Data and wifi should be off before starting any test. + return getSettings(SETTINGS_KEY_DATA_ENABLED) + .then(value => { + is(value, false, "Data must be off"); + }) + .then(() => ensureWifiEnabled(false)); +} + +function testAllNetworkInfo(aAnyConnected) { + log("= testAllNetworkInfo = " + aAnyConnected); + + let allNetworkInfo = networkManager.allNetworkInfo; + ok(allNetworkInfo, "NetworkManager.allNetworkInfo"); + + let count = Object.keys(allNetworkInfo).length; + ok(count > 0, "NetworkManager.allNetworkInfo count"); + + let connected = false; + for (let networkId in allNetworkInfo) { + if (allNetworkInfo.hasOwnProperty(networkId)) { + let networkInfo = allNetworkInfo[networkId]; + if (networkInfo.state == Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED) { + connected = true; + break; + } + } + } + + is(aAnyConnected, connected, "NetworkManager.allNetworkInfo any connected"); +} + +// Start test +startTestBase(function() { + + let origApnSettings, origWifiEnabled; + return Promise.resolve() + .then(() => { + origWifiEnabled = wifiManager.enabled; + }) + .then(() => verifyInitialState()) + .then(() => getSettings(SETTINGS_KEY_DATA_APN_SETTINGS)) + .then(value => { + origApnSettings = value; + }) + .then(() => setEmulatorAPN()) + .then(() => setDataEnabledAndWait(true)) + .then(() => testAllNetworkInfo(true)) + .then(() => setDataEnabledAndWait(false)) + .then(() => testAllNetworkInfo(false)) + // Restore original apn settings and wifi state. + .then(() => { + if (origApnSettings) { + return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, origApnSettings); + } + }) + .then(() => ensureWifiEnabled(origWifiEnabled)); +}); diff --git a/dom/system/gonk/tests/marionette/test_data_connection.js b/dom/system/gonk/tests/marionette/test_data_connection.js new file mode 100644 index 000000000..5a53b1e5f --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_data_connection.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = "head.js"; + +function setEmulatorAPN() { + let apn = [ + [{"carrier":"T-Mobile US", + "apn":"epc.tmobile.com", + "mmsc":"http://mms.msg.eng.t-mobile.com/mms/wapenc", + "types":["default","supl","mms","ims","dun", "fota"]}] + ]; + + return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, apn); +} + +// Test initial State +function testInitialState() { + log("= testInitialState ="); + + // Data should be off before starting any test. + return getSettings(SETTINGS_KEY_DATA_ENABLED) + .then(value => { + is(value, false, "Data must be off"); + }); +} + +// Test default data Connection +function testDefaultDataConnection() { + log("= testDefaultDataConnection ="); + + // Enable default data + return setDataEnabledAndWait(true) + // Disable default data + .then(() => setDataEnabledAndWait(false)); +} + +// Test non default data connection +function testNonDefaultDataConnection() { + log("= testNonDefaultDataConnection ="); + + function doTestNonDefaultDataConnection(type) { + log("doTestNonDefaultDataConnection: " + type); + + return setupDataCallAndWait(type) + .then(() => deactivateDataCallAndWait(type)); + } + + let currentApn; + return getSettings(SETTINGS_KEY_DATA_APN_SETTINGS) + .then(value => { + currentApn = value; + }) + .then(setEmulatorAPN) + .then(() => doTestNonDefaultDataConnection(NETWORK_TYPE_MOBILE_MMS)) + .then(() => doTestNonDefaultDataConnection(NETWORK_TYPE_MOBILE_SUPL)) + .then(() => doTestNonDefaultDataConnection(NETWORK_TYPE_MOBILE_IMS)) + .then(() => doTestNonDefaultDataConnection(NETWORK_TYPE_MOBILE_DUN)) + .then(() => doTestNonDefaultDataConnection(NETWORK_TYPE_MOBILE_FOTA)) + // Restore APN settings + .then(() => setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, currentApn)); +} + +// Start test +startTestBase(function() { + return testInitialState() + .then(() => testDefaultDataConnection()) + .then(() => testNonDefaultDataConnection()); +}); diff --git a/dom/system/gonk/tests/marionette/test_data_connection_proxy.js b/dom/system/gonk/tests/marionette/test_data_connection_proxy.js new file mode 100644 index 000000000..a99187538 --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_data_connection_proxy.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = "head.js"; + +const HTTP_PROXY = "10.0.2.200"; +const HTTP_PROXY_PORT = "8080"; +const MANUAL_PROXY_CONFIGURATION = 1; + +// Test initial State +function verifyInitialState() { + log("= verifyInitialState ="); + + // Data should be off before starting any test. + return getSettings(SETTINGS_KEY_DATA_ENABLED) + .then(value => { + is(value, false, "Data must be off"); + }); +} + +function setTestApn() { + let apn = [ + [ {"carrier": "T-Mobile US", + "apn": "epc.tmobile.com", + "proxy": HTTP_PROXY, + "port": HTTP_PROXY_PORT, + "mmsc": "http://mms.msg.eng.t-mobile.com/mms/wapenc", + "types": ["default","supl","mms"]} ] + ]; + + return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, apn); +} + +function waitForHttpProxyVerified(aShouldBeSet) { + let TIME_OUT_VALUE = 20000; + + return new Promise(function(aResolve, aReject) { + try { + waitFor(aResolve, () => { + let proxyType = SpecialPowers.getIntPref("network.proxy.type"); + let httpProxy = SpecialPowers.getCharPref("network.proxy.http"); + let sslProxy = SpecialPowers.getCharPref("network.proxy.ssl"); + let httpProxyPort = SpecialPowers.getIntPref("network.proxy.http_port"); + let sslProxyPort = SpecialPowers.getIntPref("network.proxy.ssl_port"); + + if ((aShouldBeSet && + proxyType == MANUAL_PROXY_CONFIGURATION && + httpProxy == HTTP_PROXY && + sslProxy == HTTP_PROXY && + httpProxyPort == HTTP_PROXY_PORT && + sslProxyPort == HTTP_PROXY_PORT) || + (!aShouldBeSet && proxyType != MANUAL_PROXY_CONFIGURATION && + !httpProxy && !sslProxy && !httpProxyPort && !sslProxyPort)) { + return true; + } + + return false; + }, TIME_OUT_VALUE); + } catch(aError) { + // Timed out. + aReject(aError); + } + }); +} + +function testDefaultDataHttpProxy() { + log("= testDefaultDataHttpProxy ="); + + return setDataEnabledAndWait(true) + .then(() => waitForHttpProxyVerified(true)) + .then(() => setDataEnabledAndWait(false)) + .then(() => waitForHttpProxyVerified(false)); +} + +function testNonDefaultDataHttpProxy(aType) { + log("= testNonDefaultDataHttpProxy - " + aType + " ="); + + return setupDataCallAndWait(aType) + // Http proxy should not be set for non-default data connections. + .then(() => waitForHttpProxyVerified(false)) + .then(() => deactivateDataCallAndWait(aType)); +} + +// Start test +startTestBase(function() { + let origApnSettings; + return verifyInitialState() + .then(() => getSettings(SETTINGS_KEY_DATA_APN_SETTINGS)) + .then(value => { + origApnSettings = value; + }) + .then(() => setTestApn()) + .then(() => testDefaultDataHttpProxy()) + .then(() => testNonDefaultDataHttpProxy(NETWORK_TYPE_MOBILE_MMS)) + .then(() => testNonDefaultDataHttpProxy(NETWORK_TYPE_MOBILE_SUPL)) + // Restore APN settings + .then(() => setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, origApnSettings)); +}); diff --git a/dom/system/gonk/tests/marionette/test_dsds_numRadioInterfaces.js b/dom/system/gonk/tests/marionette/test_dsds_numRadioInterfaces.js new file mode 100644 index 000000000..e178b8b65 --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_dsds_numRadioInterfaces.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_CONTEXT = "chrome"; + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/systemlibs.js"); + +const NS_RIL_CONTRACTID = "@mozilla.org/ril;1"; + +const PROP_RO_MOZ_RIL_NUMCLIENTS = "ro.moz.ril.numclients"; + +const PREF_RIL_NUM_RADIO_INTERFACES = "ril.numRadioInterfaces"; + +ok(libcutils, "libcutils is available"); + +var propNum = (function() { + try { + let numString = libcutils.property_get(PROP_RO_MOZ_RIL_NUMCLIENTS, "1"); + let num = parseInt(numString, 10); + if (num >= 0) { + return num; + } + } catch (e) {} +})(); + +log("Retrieved '" + PROP_RO_MOZ_RIL_NUMCLIENTS + "' = " + propNum); +ok(propNum, PROP_RO_MOZ_RIL_NUMCLIENTS); + +var prefNum = Services.prefs.getIntPref(PREF_RIL_NUM_RADIO_INTERFACES); +log("Retrieved '" + PREF_RIL_NUM_RADIO_INTERFACES + "' = " + prefNum); + +var ril = Cc[NS_RIL_CONTRACTID].getService(Ci.nsIRadioInterfaceLayer); +ok(ril, "ril.constructor is " + ril.constructor); + +var ifaceNum = ril.numRadioInterfaces; +log("Retrieved 'nsIRadioInterfaceLayer.numRadioInterfaces' = " + ifaceNum); + +is(propNum, prefNum); +is(propNum, ifaceNum); + +finish(); diff --git a/dom/system/gonk/tests/marionette/test_fakevolume.js b/dom/system/gonk/tests/marionette/test_fakevolume.js new file mode 100644 index 000000000..173f9ac11 --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_fakevolume.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 10000; + +var Cc = SpecialPowers.Cc; +var Ci = SpecialPowers.Ci; + +var volumeService = Cc["@mozilla.org/telephony/volume-service;1"].getService(Ci.nsIVolumeService); +ok(volumeService, "Should have volume service"); + +var volName = "fake"; +var mountPoint = "/data/fake/storage"; +volumeService.createFakeVolume(volName, mountPoint); + +var vol = volumeService.getVolumeByName(volName); +ok(vol, "volume shouldn't be null"); + +is(volName, vol.name, "name"); +is(mountPoint, vol.mountPoint, "moutnPoint"); +is(Ci.nsIVolume.STATE_MOUNTED, vol.state, "state"); + +ok(vol.mountGeneration > 0, "mount generation should not be zero"); + +finish(); diff --git a/dom/system/gonk/tests/marionette/test_geolocation.js b/dom/system/gonk/tests/marionette/test_geolocation.js new file mode 100644 index 000000000..201c8b3e3 --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_geolocation.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 10000; + +var geolocation = window.navigator.geolocation; +ok(geolocation); + +var sample = []; +var result = []; +var wpid; + +/** + * Grant special power to get the geolocation + */ +SpecialPowers.addPermission("geolocation", true, document); + +/** + * Disable wifi geolocation provider + */ +wifiUri = SpecialPowers.getCharPref("geo.wifi.uri"); +SpecialPowers.setCharPref("geo.wifi.uri", "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs?action=stop-responding"); + +/** + * Helper that compares the geolocation against the web API. + */ +function verifyLocation() { + + log("Sample:" + sample.join(',')); + log("Result:" + result.join(',')); + + for (i in sample) { + is(sample.pop(), result.pop()); + } + + window.setTimeout(cleanup, 0); +} + +/** + * Test story begins here. + */ +function setup() { + log("Providing initial setup: set geographic position watcher."); + + + wpid = geolocation.watchPosition(function(position) { + log("Position changes: (" + position.coords.latitude + "/" + position.coords.longitude + ")"); + result.push(""+position.coords.latitude + "/" + position.coords.longitude); + }); + + lat = 0; + lon = 0; + + cmd = "geo fix " + lon + " " + lat; + sample.push(lat+"/"+lon); + + runEmulatorCmd(cmd, function(result) { + window.setTimeout(movePosition_1, 0); + }); +} + +function movePosition_1() { + log("Geolocation changes. Move to Position 1."); + + lat = 25; + lon = 121.56499833333334; + + cmd = "geo fix " + lon + " " + lat; + sample.push(lat+"/"+lon); + + runEmulatorCmd(cmd, function(result) { + window.setTimeout(movePosition_2, 0); + }); +} + +function movePosition_2() { + log("Geolocation changes to a negative longitude. Move to Position 2."); + + lat = 37.393; + lon = -122.08199833333335; + + cmd = "geo fix " + lon + " " + lat; + sample.push(lat+"/"+lon); + + runEmulatorCmd(cmd, function(result) { + window.setTimeout(movePosition_3, 0); + }); +} + +function movePosition_3() { + log("Geolocation changes with WatchPosition. Move to Position 3."); + + lat = -22; + lon = -43; + + cmd = "geo fix " + lon + " " + lat; + sample.push(lat+"/"+lon); + + geolocation.getCurrentPosition(function(position) { + log("getCurrentPosition: Expected location: ("+lat+"/"+lon+"); Current location: (" + position.coords.latitude + "/" + position.coords.longitude + ")"); + is(lat, position.coords.latitude); + is(lon, position.coords.longitude); + }); + + runEmulatorCmd(cmd, function(result) { + window.setTimeout(verifyLocation, 0); + }); +} + +function cleanup() { + geolocation.clearWatch(wpid); + SpecialPowers.removePermission("geolocation", document); + SpecialPowers.setCharPref("geo.wifi.uri", wifiUri); + finish(); +} + +setup(); diff --git a/dom/system/gonk/tests/marionette/test_multiple_data_connection.js b/dom/system/gonk/tests/marionette/test_multiple_data_connection.js new file mode 100644 index 000000000..24abd4451 --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_multiple_data_connection.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = "head.js"; + +// Must sync with hardware/ril/reference-ril/reference-ril.c +const MAX_DATA_CONTEXTS = 4; + +function setEmulatorAPN() { + // Use different apn for each network type. + let apn = [[ { "carrier":"T-Mobile US", + "apn":"epc1.tmobile.com", + "types":["default"] }, + { "carrier":"T-Mobile US", + "apn":"epc2.tmobile.com", + "mmsc":"http://mms.msg.eng.t-mobile.com/mms/wapenc", + "types":["mms"] }, + { "carrier":"T-Mobile US", + "apn":"epc3.tmobile.com", + "types":["supl"] }, + { "carrier":"T-Mobile US", + "apn":"epc4.tmobile.com", + "types":["ims"] }, + { "carrier":"T-Mobile US", + "apn":"epc5.tmobile.com", + "types":["dun"] }, + { "carrier":"T-Mobile US", + "apn":"epc6.tmobile.com", + "types":["fota"] }]]; + + return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, apn); +} + +// Test initial State +function testInitialState() { + log("= testInitialState ="); + + // Data should be off before starting any test. + return getSettings(SETTINGS_KEY_DATA_ENABLED) + .then(value => { + is(value, false, "Data must be off"); + }); +} + +function testSetupConcurrentDataCalls() { + log("= testSetupConcurrentDataCalls ="); + + let promise = Promise.resolve(); + // Skip default mobile type. + for (let i = 1; i < MAX_DATA_CONTEXTS; i++) { + let type = networkTypes[i]; + promise = promise.then(() => setupDataCallAndWait(type)); + } + return promise; +} + +function testDeactivateConcurrentDataCalls() { + log("= testDeactivateConcurrentDataCalls ="); + + let promise = Promise.resolve(); + // Skip default mobile type. + for (let i = 1; i < MAX_DATA_CONTEXTS; i++) { + let type = networkTypes[i]; + promise = promise.then(() => deactivateDataCallAndWait(type)); + } + return promise; +} + +// Start test +startTestBase(function() { + + let origApnSettings; + return testInitialState() + .then(() => getSettings(SETTINGS_KEY_DATA_APN_SETTINGS)) + .then(value => { + origApnSettings = value; + }) + .then(() => setEmulatorAPN()) + .then(() => setDataEnabledAndWait(true)) + .then(() => testSetupConcurrentDataCalls()) + .then(() => testDeactivateConcurrentDataCalls()) + .then(() => setDataEnabledAndWait(false)) + .then(() => { + if (origApnSettings) { + return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, origApnSettings); + } + }); +}); diff --git a/dom/system/gonk/tests/marionette/test_network_active_changed.js b/dom/system/gonk/tests/marionette/test_network_active_changed.js new file mode 100644 index 000000000..5886f37ed --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_network_active_changed.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = "head.js"; + +var networkManager = + Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager); +ok(networkManager, + "networkManager.constructor is " + networkManager.constructor); + +function testInitialState() { + return getSettings(SETTINGS_KEY_DATA_ENABLED) + .then((enabled) => { + is(enabled, false, "data should be off by default"); + is(networkManager.activeNetworkInfo, null, + "networkManager.activeNetworkInfo should be null by default"); + }); +} + +function testActiveNetworkChangedBySwitchingDataCall(aDataCallEnabled) { + log("Test active network by switching dataCallEnabled to " + aDataCallEnabled); + + let promises = []; + promises.push(waitForObserverEvent(TOPIC_NETWORK_ACTIVE_CHANGED)); + promises.push(setSettings(SETTINGS_KEY_DATA_ENABLED, aDataCallEnabled)); + + return Promise.all(promises).then(function(results) { + let subject = results[0]; + + if (aDataCallEnabled) { + ok(subject instanceof Ci.nsINetworkInfo, + "subject should be an instance of nsINetworkInfo"); + ok(subject instanceof Ci.nsIRilNetworkInfo, + "subject should be an instance of nsIRilNetworkInfo"); + is(subject.type, NETWORK_TYPE_MOBILE, + "subject.type should be NETWORK_TYPE_MOBILE"); + } + + is(subject, networkManager.activeNetworkInfo, + "subject should be equal with networkManager.activeNetworkInfo"); + }); +} + +// Start test +startTestBase(function() { + return testInitialState() + // Test active network changed by enabling data call. + .then(() => testActiveNetworkChangedBySwitchingDataCall(true)) + // Test active network changed by disabling data call. + .then(() => testActiveNetworkChangedBySwitchingDataCall(false)); +}); diff --git a/dom/system/gonk/tests/marionette/test_network_interface_list_service.js b/dom/system/gonk/tests/marionette/test_network_interface_list_service.js new file mode 100644 index 000000000..549940fa5 --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_network_interface_list_service.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = "head.js"; + +function getNetworkInfo(aType) { + let networkListService = + Cc["@mozilla.org/network/interface-list-service;1"]. + getService(Ci.nsINetworkInterfaceListService); + // Get all available interfaces + let networkList = networkListService.getDataInterfaceList(0); + + // Try to get nsINetworkInterface for aType. + let numberOfInterface = networkList.getNumberOfInterface(); + for (let i = 0; i < numberOfInterface; i++) { + let info = networkList.getInterfaceInfo(i); + if (info.type === aType) { + return info; + } + } + + return null; +} + +// Test getDataInterfaceList by enabling/disabling mobile data. +function testGetDataInterfaceList(aMobileDataEnabled) { + log("Test getDataInterfaceList with mobile data " + + aMobileDataEnabled ? "enabled" : "disabled"); + + return setDataEnabledAndWait(aMobileDataEnabled) + .then(() => getNetworkInfo(NETWORK_TYPE_MOBILE)) + .then((networkInfo) => { + if (!networkInfo) { + ok(false, "Should get an valid nsINetworkInfo for mobile"); + return; + } + + ok(networkInfo instanceof Ci.nsINetworkInfo, + "networkInfo should be an instance of nsINetworkInfo"); + + let ipAddresses = {}; + let prefixs = {}; + let numOfGateways = {}; + let numOfDnses = {}; + let numOfIpAddresses = networkInfo.getAddresses(ipAddresses, prefixs); + let gateways = networkInfo.getGateways(numOfGateways); + let dnses = networkInfo.getDnses(numOfDnses); + + if (aMobileDataEnabled) { + // Mobile data is enabled. + is(networkInfo.state, NETWORK_STATE_CONNECTED, "check state"); + ok(numOfIpAddresses > 0, "check number of ipAddresses"); + ok(ipAddresses.value.length > 0, "check ipAddresses.length"); + ok(prefixs.value.length > 0, "check prefixs.length"); + ok(numOfGateways.value > 0, "check number of gateways"); + ok(prefixs.value.length > 0, "check prefixs.length"); + ok(gateways.length > 0, "check gateways.length"); + ok(numOfDnses.value > 0, "check number of dnses"); + ok(dnses.length > 0, "check dnses.length"); + } else { + // Mobile data is disabled. + is(networkInfo.state, NETWORK_STATE_DISCONNECTED, "check state"); + is(numOfIpAddresses, 0, "check number of ipAddresses"); + is(ipAddresses.value.length, 0, "check ipAddresses.length"); + is(prefixs.value.length, 0, "check prefixs.length"); + is(numOfGateways.value, 0, "check number of gateways"); + is(prefixs.value.length, 0, "check prefixs.length"); + is(gateways.length, 0, "check gateways.length"); + is(numOfDnses.value, 0, "check number of dnses"); + is(dnses.length, 0, "check dnses.length"); + } + }); +} + +// Start test +startTestBase(function() { + return Promise.resolve() + // Test initial State + .then(() => { + log("Test initial state"); + + // Data should be off before starting any test. + return getSettings(SETTINGS_KEY_DATA_ENABLED) + .then(value => { + is(value, false, "Mobile data must be off"); + }); + }) + + // Test getDataInterfaceList with mobile data enabled. + .then(() => testGetDataInterfaceList(true)) + + // Test getDataInterfaceList with mobile data disabled. + .then(() => testGetDataInterfaceList(false)); +}); diff --git a/dom/system/gonk/tests/marionette/test_network_interface_mtu.js b/dom/system/gonk/tests/marionette/test_network_interface_mtu.js new file mode 100644 index 000000000..679efe2ed --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_network_interface_mtu.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = "head.js"; + +const TEST_MTU1 = "1410"; +const TEST_MTU2 = "1440"; + +function setEmulatorAPN() { + let apn = [ + [ { "carrier":"T-Mobile US", + "apn":"epc1.tmobile.com", + "types":["default"], + "mtu": TEST_MTU1 }, + { "carrier":"T-Mobile US", + "apn":"epc2.tmobile.com", + "mmsc":"http://mms.msg.eng.t-mobile.com/mms/wapenc", + "types":["supl","mms","ims","dun", "fota"], + "mtu": TEST_MTU2 } ] + ]; + + return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, apn); +} + +function verifyInitialState() { + // Data should be off before starting any test. + return getSettings(SETTINGS_KEY_DATA_ENABLED) + .then(value => { + is(value, false, "Data must be off"); + }); +} + +function verifyMtu(aInterfaceName, aMtu) { + return runEmulatorShellCmdSafe(['ip', 'link', 'show', 'dev', aInterfaceName]) + .then(aLines => { + // Sample output: + // + // 4: rmnet0: <BROADCAST,MULTICAST> mtu 1410 qdisc pfifo_fast state DOWN mode DEFAULT qlen 1000 + // link/ether 52:54:00:12:34:58 brd ff:ff:ff:ff:ff:ff + // + let mtu; + aLines.some(function (aLine) { + let tokens = aLine.trim().split(/\s+/); + let mtuIndex = tokens.indexOf('mtu'); + if (mtuIndex < 0 || mtuIndex + 1 >= tokens.length) { + return false; + } + + mtu = tokens[mtuIndex + 1]; + return true; + }); + + is(mtu, aMtu, aInterfaceName + "'s mtu."); + }); +} + +function testDefaultDataCallMtu() { + log("= testDefaultDataCallMtu ="); + + return setDataEnabledAndWait(true) + .then(aNetworkInfo => verifyMtu(aNetworkInfo.name, TEST_MTU1)) + .then(() => setDataEnabledAndWait(false)); +} + +function testNonDefaultDataCallMtu() { + log("= testNonDefaultDataCallMtu ="); + + function doTestNonDefaultDataCallMtu(aType) { + log("doTestNonDefaultDataCallMtu: " + aType); + + return setupDataCallAndWait(aType) + .then(aNetworkInfo => verifyMtu(aNetworkInfo.name, TEST_MTU2)) + .then(() => deactivateDataCallAndWait(aType)); + } + + return doTestNonDefaultDataCallMtu(NETWORK_TYPE_MOBILE_MMS) + .then(() => doTestNonDefaultDataCallMtu(NETWORK_TYPE_MOBILE_SUPL)) + .then(() => doTestNonDefaultDataCallMtu(NETWORK_TYPE_MOBILE_IMS)) + .then(() => doTestNonDefaultDataCallMtu(NETWORK_TYPE_MOBILE_DUN)) + .then(() => doTestNonDefaultDataCallMtu(NETWORK_TYPE_MOBILE_FOTA)); +} + +// Start test +startTestBase(function() { + let origApnSettings; + return verifyInitialState() + .then(() => getSettings(SETTINGS_KEY_DATA_APN_SETTINGS)) + .then(value => { + origApnSettings = value; + }) + .then(() => setEmulatorAPN()) + .then(() => testDefaultDataCallMtu()) + .then(() => testNonDefaultDataCallMtu()) + .then(() => { + if (origApnSettings) { + return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, origApnSettings); + } + }); +}); diff --git a/dom/system/gonk/tests/marionette/test_ril_code_quality.py b/dom/system/gonk/tests/marionette/test_ril_code_quality.py new file mode 100644 index 000000000..d741d8a2e --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_ril_code_quality.py @@ -0,0 +1,371 @@ +""" +The test performs the static code analysis check by JSHint. + +Target js files: +- RadioInterfaceLayer.js +- ril_worker.js +- ril_consts.js + +If the js file contains the line of 'importScript()' (Ex: ril_worker.js), the +test will perform a special merge step before excuting JSHint. + +Ex: Script A +-------------------------------- +importScripts('Script B') +... +-------------------------------- + +We merge these two scripts into one by the following way. + +-------------------------------- +[Script B (ex: ril_consts.js)] +(function(){ [Script A (ex: ril_worker.js)] +})(); +-------------------------------- + +Script A (ril_worker.js) runs global strict mode. +Script B (ril_consts.js) not. + +The above merge way ensures the correct scope of 'strict mode.' +""" + +import bisect +import inspect +import os +import os.path +import re +import unicodedata + +from marionette_harness import MarionetteTestCase + + +class StringUtility: + + """A collection of some string utilities.""" + + @staticmethod + def find_match_lines(lines, pattern): + """Return a list of lines that contains given pattern.""" + return [line for line in lines if pattern in line] + + @staticmethod + def remove_non_ascii(data): + """Remove non ascii characters in data and return it as new string.""" + if type(data).__name__ == 'unicode': + data = unicodedata.normalize( + 'NFKD', data).encode('ascii', 'ignore') + return data + + @staticmethod + def auto_close(lines): + """Ensure every line ends with '\n'.""" + if lines and not lines[-1].endswith('\n'): + lines[-1] += '\n' + return lines + + @staticmethod + def auto_wrap_strict_mode(lines): + """Wrap by function scope if lines contain 'use strict'.""" + if StringUtility.find_match_lines(lines, 'use strict'): + lines[0] = '(function(){' + lines[0] + lines.append('})();\n') + return lines + + @staticmethod + def get_imported_list(lines): + """Get a list of imported items.""" + return [item + for line in StringUtility.find_match_lines(lines, 'importScripts') + for item in StringUtility._get_imported_list_from_line(line)] + + @staticmethod + def _get_imported_list_from_line(line): + """Extract all items from 'importScripts(...)'. + + importScripts("ril_consts.js", "systemlibs.js") + => ['ril_consts', 'systemlibs.js'] + + """ + pattern = re.compile(r'\s*importScripts\((.*)\)') + m = pattern.match(line) + if not m: + raise Exception('Parse importScripts error.') + return [name.translate(None, '\' "') for name in m.group(1).split(',')] + + +class ResourceUriFileReader: + + """Handle the process of reading the source code from system.""" + + URI_PREFIX = 'resource://gre/' + URI_PATH = { + 'RadioInterfaceLayer.js': 'components/RadioInterfaceLayer.js', + 'ril_worker.js': 'modules/ril_worker.js', + 'ril_consts.js': 'modules/ril_consts.js', + 'systemlibs.js': 'modules/systemlibs.js', + 'worker_buf.js': 'modules/workers/worker_buf.js', + } + + CODE_OPEN_CHANNEL_BY_URI = ''' + var Cc = Components.classes; + var Ci = Components.interfaces; + var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager); + global.uri = '%(uri)s'; + global.channel = ios.newChannel2(global.uri, + null, + null, + null, // aLoadingNode + secMan.getSystemPrincipal(), + null, // aTriggeringPrincipal + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + ''' + + CODE_GET_SPEC = ''' + return global.channel.URI.spec; + ''' + + CODE_READ_CONTENT = ''' + var Cc = Components.classes; + var Ci = Components.interfaces; + + var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader); + var inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); + + var jaruri = global.channel.URI.QueryInterface(Ci.nsIJARURI); + var file = jaruri.JARFile.QueryInterface(Ci.nsIFileURL).file; + var entry = jaruri.JAREntry; + zipReader.open(file); + inputStream.init(zipReader.getInputStream(entry)); + var content = inputStream.read(inputStream.available()); + inputStream.close(); + zipReader.close(); + return content; + ''' + + @classmethod + def get_uri(cls, filename): + """Convert filename to URI in system.""" + if filename.startswith(cls.URI_PREFIX): + return filename + else: + return cls.URI_PREFIX + cls.URI_PATH[filename] + + def __init__(self, marionette): + self.runjs = lambda x: marionette.execute_script(x, + new_sandbox=False, + sandbox='system') + + def read_file(self, filename): + """Read file and return the contents as string.""" + content = self._read_uri(self.get_uri(filename)) + content = content.replace('"use strict";', '') + return StringUtility.remove_non_ascii(content) + + def _read_uri(self, uri): + """Read URI in system and return the contents as string.""" + # Open the uri as a channel. + self.runjs(self.CODE_OPEN_CHANNEL_BY_URI % {'uri': uri}) + + # Make sure spec is a jar uri, and not recursive. + # Ex: 'jar:file:///system/b2g/omni.ja!/modules/ril_worker.js' + # + # For simplicity, we don't handle other special cases in this test. + # If B2G build system changes in the future, such as put the jar in + # another jar, the test case will fail. + spec = self.runjs(self.CODE_GET_SPEC) + if (not spec.startswith('jar:file://')) or (spec.count('jar:') != 1): + raise Exception('URI resolve error') + + # Read the content from channel. + content = self.runjs(self.CODE_READ_CONTENT) + return content + + +class JSHintEngine: + + """Invoke jshint script on system.""" + + CODE_INIT_JSHINT = ''' + %(script)s; + global.JSHINT = JSHINT; + global.options = JSON.parse(%(config_string)s); + global.globals = global.options.globals; + delete global.options.globals; + ''' + + CODE_RUN_JSHINT = ''' + global.script = %(code)s; + return global.JSHINT(global.script, global.options, global.globals); + ''' + + CODE_GET_JSHINT_ERROR = ''' + return global.JSHINT.errors; + ''' + + def __init__(self, marionette, script, config): + # Remove single line comment in config. + config = '\n'.join([line.partition('//')[0] + for line in config.splitlines()]) + + # Set global (JSHINT, options, global) in js environment. + self.runjs = lambda x: marionette.execute_script(x, + new_sandbox=False, + sandbox='system') + self.runjs(self.CODE_INIT_JSHINT % + {'script': script, 'config_string': repr(config)}) + + def run(self, code, filename=''): + """Excute JShint check for the given code.""" + check_pass = self.runjs(self.CODE_RUN_JSHINT % {'code': repr(code)}) + errors = self.runjs(self.CODE_GET_JSHINT_ERROR) + return check_pass, self._get_error_messages(errors, filename) + + def _get_error_messages(self, errors, filename=''): + """ + Convert an error object to a list of readable string. + + [{"a": null, "c": null, "code": "W033", "d": null, "character": 6, + "evidence": "var a", "raw": "Missing semicolon.", + "reason": "Missing semicolon.", "b": null, "scope": "(main)", "line": 1, + "id": "(error)"}] + => line 1, col 6, Missing semicolon. + + """ + LINE, COL, REASON = u'line', u'character', u'reason' + return ["%s: line %s, col %s, %s" % + (filename, error[LINE], error[COL], error[REASON]) + for error in errors if error] + + +class Linter: + + """Handle the linting related process.""" + + def __init__(self, code_reader, jshint, reporter=None): + """Set the linter with code_reader, jshint engine, and reporter. + + Should have following functionality. + - code_reader.read_file(filename) + - jshint.run(code, filename) + - reporter([...]) + + """ + self.code_reader = code_reader + self.jshint = jshint + if reporter is None: + self.reporter = lambda x: '\n'.join(x) + else: + self.reporter = reporter + + def lint_file(self, filename): + """Lint the file and return (pass, error_message).""" + # Get code contents. + code = self.code_reader.read_file(filename) + lines = code.splitlines() + import_list = StringUtility.get_imported_list(lines) + if not import_list: + check_pass, error_message = self.jshint.run(code, filename) + else: + newlines, info = self._merge_multiple_codes(filename, import_list) + # Each line of |newlines| contains '\n'. + check_pass, error_message = self.jshint.run(''.join(newlines)) + error_message = self._convert_merged_result(error_message, info) + # Only keep errors for this file. + error_message = [line for line in error_message + if line.startswith(filename)] + check_pass = (len(error_message) == 0) + return check_pass, self.reporter(error_message) + + def _merge_multiple_codes(self, filename, import_list): + """Merge multiple codes from filename and import_list.""" + dirname, filename = os.path.split(filename) + dst_line = 1 + dst_results = [] + info = [] + + # Put the imported script first, and then the original script. + for f in import_list + [filename]: + filepath = os.path.join(dirname, f) + + # Maintain a mapping table. + # New line number after merge => original file and line number. + info.append((dst_line, filepath, 1)) + try: + code = self.code_reader.read_file(filepath) + lines = code.splitlines(True) # Keep '\n'. + src_results = StringUtility.auto_wrap_strict_mode( + StringUtility.auto_close(lines)) + dst_results.extend(src_results) + dst_line += len(src_results) + except: + info.pop() + return dst_results, info + + def _convert_merged_result(self, error_lines, line_info): + pattern = re.compile(r'(.*): line (\d+),(.*)') + start_line = [info[0] for info in line_info] + new_result_lines = [] + for line in error_lines: + m = pattern.match(line) + if not m: + continue + + line_number, remain = int(m.group(2)), m.group(3) + + # [1, 2, 7, 8] + # ^ for 7, pos = 3 + # ^ for 6, pos = 2 + pos = bisect.bisect_right(start_line, line_number) + dst_line, name, src_line = line_info[pos - 1] + real_line_number = line_number - dst_line + src_line + new_result_lines.append( + "%s: line %s,%s" % (name, real_line_number, remain)) + return new_result_lines + + +class TestRILCodeQuality(MarionetteTestCase): + + JSHINT_PATH = 'ril_jshint/jshint.js' + JSHINTRC_PATH = 'ril_jshint/jshintrc' + + def _read_local_file(self, filepath): + """Read file content from local (folder of this test case).""" + test_dir = os.path.dirname(inspect.getfile(TestRILCodeQuality)) + return open(os.path.join(test_dir, filepath)).read() + + def _get_extended_error_message(self, error_message): + return '\n'.join(['See errors below and more information in Bug 880643', + '\n'.join(error_message), + 'See errors above and more information in Bug 880643']) + + def _check(self, filename): + check_pass, error_message = self.linter.lint_file(filename) + self.assertTrue(check_pass, error_message) + + def setUp(self): + MarionetteTestCase.setUp(self) + self.linter = Linter( + ResourceUriFileReader(self.marionette), + JSHintEngine(self.marionette, + self._read_local_file(self.JSHINT_PATH), + self._read_local_file(self.JSHINTRC_PATH)), + self._get_extended_error_message) + + def tearDown(self): + MarionetteTestCase.tearDown(self) + + def test_RadioInterfaceLayer(self): + self._check('RadioInterfaceLayer.js') + + # Bug 936504. Disable the test for 'ril_worker.js'. It sometimes runs very + # slow and causes the timeout fail on try server. + #def test_ril_worker(self): + # self._check('ril_worker.js') + + def test_ril_consts(self): + self._check('ril_consts.js') + + def test_worker_buf(self): + self._check('worker_buf.js') diff --git a/dom/system/gonk/tests/marionette/test_screen_state.js b/dom/system/gonk/tests/marionette/test_screen_state.js new file mode 100644 index 000000000..2281412d5 --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_screen_state.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 10000; + +var Services = SpecialPowers.Services; + +function testScreenState(on, expected, msg) { + // send event to RadioInterface + Services.obs.notifyObservers(null, 'screen-state-changed', on); + // maybe rild/qemu needs some time to process the event + window.setTimeout(function() { + runEmulatorCmd('gsm report creg', function(result) { + is(result.pop(), 'OK', '\'gsm report creg\' successful'); + ok(result.indexOf(expected) !== -1, msg); + runNextTest(); + })}, 1000); +} + +function testScreenStateDisabled() { + testScreenState('off', '+CREG: 1', 'screen is disabled'); +} + +function testScreenStateEnabled() { + testScreenState('on', '+CREG: 2', 'screen is enabled'); +} + +var tests = [ + testScreenStateDisabled, + testScreenStateEnabled +]; + +function runNextTest() { + let test = tests.shift(); + if (!test) { + cleanUp(); + return; + } + + test(); +} + +function cleanUp() { + finish(); +} + +runNextTest(); diff --git a/dom/system/gonk/tests/marionette/test_timezone_changes.js b/dom/system/gonk/tests/marionette/test_timezone_changes.js new file mode 100644 index 000000000..11dbaec5a --- /dev/null +++ b/dom/system/gonk/tests/marionette/test_timezone_changes.js @@ -0,0 +1,135 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +MARIONETTE_TIMEOUT = 60000; +MARIONETTE_HEAD_JS = 'head.js'; + +function init() { + let promises = []; + + /* + * The initial timezone of the emulator could be anywhere, depends the host + * machine. Ensure resetting it to UTC before testing. + */ + promises.push(runEmulatorCmdSafe('gsm timezone 0')); + promises.push(new Promise((aResolve, aReject) => { + waitFor(aResolve, () => { + return new Date().getTimezoneOffset() === 0; + }); + })); + + return Promise.all(promises); +} + +function paddingZeros(aNumber, aLength) { + let str = '' + aNumber; + while (str.length < aLength) { + str = '0' + str; + } + + return str; +} + +function verifyDate(aTestDate, aUTCOffsetDate) { + // Verify basic properties. + is(aUTCOffsetDate.getUTCFullYear(), aTestDate.getFullYear(), 'year'); + is(aUTCOffsetDate.getUTCMonth(), aTestDate.getMonth(), 'month'); + is(aUTCOffsetDate.getUTCDate(), aTestDate.getDate(), 'date'); + is(aUTCOffsetDate.getUTCHours(), aTestDate.getHours(), 'hours'); + is(aUTCOffsetDate.getUTCMinutes(), aTestDate.getMinutes(), 'minutes'); + is(aUTCOffsetDate.getUTCMilliseconds(), aTestDate.getMilliseconds(), 'milliseconds'); + + // Ensure toLocaleString also uses correct timezone. + // It uses ICU's timezone instead of the offset calculated from gecko prtime. + let expectedDateString = + paddingZeros(aUTCOffsetDate.getUTCMonth() + 1, 2) + '/' + + paddingZeros(aUTCOffsetDate.getUTCDate(), 2); + let dateString = aTestDate.toLocaleString('en-US', { + month: '2-digit', + day: '2-digit', + }); + let expectedTimeString = + paddingZeros(aUTCOffsetDate.getUTCHours(), 2) + ':' + + paddingZeros(aUTCOffsetDate.getUTCMinutes(), 2); + let timeString = aTestDate.toLocaleString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit' + }); + + is(expectedDateString, dateString, 'dateString'); + is(expectedTimeString, timeString, 'timeString'); +} + +function waitForTimezoneUpdate(aTzOffset, + aTestDateInMillis = 86400000, // Use 'UTC 00:00:00, 2nd of Jan, 1970' by default. + aTransTzOffset, aTransTestDateInMillis) { + return new Promise(function(aResolve, aReject) { + window.addEventListener('moztimechange', function onevent(aEvent) { + // Since there could be multiple duplicate moztimechange event, wait until + // timezone is actually changed to expected value before removing the + // listener. + let testDate = new Date(aTestDateInMillis); + if (testDate.getTimezoneOffset() === aTzOffset) { + window.removeEventListener('moztimechange', onevent); + + // The UTC time of offsetDate is the same as the expected local time of + // testDate. We'll use it to verify the values. + let offsetDate = new Date(aTestDateInMillis - aTzOffset * 60 * 1000); + verifyDate(testDate, offsetDate); + + // Verify transition time if given. + if (aTransTzOffset !== undefined) { + testDate = new Date(aTransTestDateInMillis); + is(testDate.getTimezoneOffset(), aTransTzOffset); + + // Verify transition date. + offsetDate = new Date(aTransTestDateInMillis - aTransTzOffset * 60 * 1000); + verifyDate(testDate, offsetDate); + } + + aResolve(aEvent); + } + }); + }); +} + +function testChangeNitzTimezone(aTzDiff) { + let promises = []; + + // aTzOffset should be the expected value for getTimezoneOffset(). + // Note that getTimezoneOffset() is not so straightforward, + // it values (UTC - localtime), so UTC+08:00 returns -480. + promises.push(waitForTimezoneUpdate(-aTzDiff * 15)); + promises.push(runEmulatorCmdSafe('gsm timezone ' + aTzDiff)); + + return Promise.all(promises); +} + +function testChangeOlsonTimezone(aOlsonTz, aTzOffset, aTestDateInMillis, + aTransTzOffset, aTransTestDateInMillis) { + let promises = []; + + promises.push(waitForTimezoneUpdate(aTzOffset, aTestDateInMillis, + aTransTzOffset, aTransTestDateInMillis)); + promises.push(setSettings('time.timezone', aOlsonTz)); + + return Promise.all(promises); +} + +// Start test +startTestBase(function() { + return init() + .then(() => testChangeNitzTimezone(36)) // UTC+09:00 + .then(() => testChangeOlsonTimezone('America/New_York', + 300, 1446357600000, // 2015/11/01 02:00 UTC-04:00 => 01:00 UTC-05:00 (EST) + 240, 1425798000000)) // 2015/03/08 02:00 UTC-05:00 => 03:00 UTC-04:00 (EDT) + .then(() => testChangeNitzTimezone(-22)) // UTC-05:30 + .then(() => testChangeNitzTimezone(51)) // UTC+12:45 + .then(() => testChangeOlsonTimezone('Australia/Adelaide', + -570, 1428165000000, // 2015/04/05 03:00 UTC+10:30 => 02:00 UTC+09:30 (ACST) + -630, 1443889800000)) // 2015/10/04 02:00 UTC+09:30 => 03:00 UTC+10:30 (ACDT) + .then(() => testChangeNitzTimezone(-38)) // UTC-09:30 + .then(() => testChangeNitzTimezone(0)) // UTC + .then(() => runEmulatorCmdSafe('gsm timezone auto')); +}); diff --git a/dom/system/gonk/tests/test_ril_system_messenger.js b/dom/system/gonk/tests/test_ril_system_messenger.js new file mode 100644 index 000000000..a588d0ddb --- /dev/null +++ b/dom/system/gonk/tests/test_ril_system_messenger.js @@ -0,0 +1,1187 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var RIL = {}; +Cu.import("resource://gre/modules/ril_consts.js", RIL); + +XPCOMUtils.defineLazyServiceGetter(this, "gStkCmdFactory", + "@mozilla.org/icc/stkcmdfactory;1", + "nsIStkCmdFactory"); + +/** + * Name space for RILSystemMessenger.jsm. Only initialized after first call to + * newRILSystemMessenger. + */ +var RSM; + +var gReceivedMsgType = null; +var gReceivedMessage = null; + +/** + * Create a new RILSystemMessenger instance. + * + * @return a RILSystemMessenger instance. + */ +function newRILSystemMessenger() { + if (!RSM) { + RSM = Cu.import("resource://gre/modules/RILSystemMessenger.jsm", {}); + equal(typeof RSM.RILSystemMessenger, "function", "RSM.RILSystemMessenger"); + } + + let rsm = new RSM.RILSystemMessenger(); + rsm.broadcastMessage = (aType, aMessage) => { + gReceivedMsgType = aType; + gReceivedMessage = aMessage; + }; + + rsm.createCommandMessage = (aStkProactiveCmd) => { + return gStkCmdFactory.createCommandMessage(aStkProactiveCmd); + }; + + return rsm; +} + +function equal_received_system_message(aType, aMessage) { + equal(aType, gReceivedMsgType); + deepEqual(aMessage, gReceivedMessage); + gReceivedMsgType = null; + gReceivedMessage = null; +} + +/** + * Verify that each nsIXxxMessenger could be retrieved. + */ +function run_test() { + let telephonyMessenger = Cc["@mozilla.org/ril/system-messenger-helper;1"] + .getService(Ci.nsITelephonyMessenger); + + let smsMessenger = Cc["@mozilla.org/ril/system-messenger-helper;1"] + .getService(Ci.nsISmsMessenger); + + let cellbroadcastMessenger = Cc["@mozilla.org/ril/system-messenger-helper;1"] + .getService(Ci.nsICellbroadcastMessenger); + + let mobileConnectionMessenger = Cc["@mozilla.org/ril/system-messenger-helper;1"] + .getService(Ci.nsIMobileConnectionMessenger); + + let iccMessenger = Cc["@mozilla.org/ril/system-messenger-helper;1"] + .getService(Ci.nsIIccMessenger); + + ok(telephonyMessenger !== null, "Get TelephonyMessenger."); + ok(smsMessenger != null, "Get SmsMessenger."); + ok(cellbroadcastMessenger != null, "Get CellbroadcastMessenger."); + ok(mobileConnectionMessenger != null, "Get MobileConnectionMessenger."); + ok(iccMessenger != null, "Get IccMessenger."); + + run_next_test(); +} + +/** + * Verify RILSystemMessenger.notifyNewCall() + */ +add_test(function test_telephony_messenger_notify_new_call() { + let messenger = newRILSystemMessenger(); + + messenger.notifyNewCall(); + equal_received_system_message("telephony-new-call", {}); + + run_next_test(); +}); + +/** + * Verify RILSystemMessenger.notifyCallEnded() + */ +add_test(function test_telephony_messenger_notify_call_ended() { + let messenger = newRILSystemMessenger(); + + messenger.notifyCallEnded(1, + "+0987654321", + null, + true, + 500, + false, + true); + + equal_received_system_message("telephony-call-ended", { + serviceId: 1, + number: "+0987654321", + emergency: true, + duration: 500, + direction: "incoming", + hangUpLocal: true + }); + + // Verify 'optional' parameter of secondNumber. + messenger.notifyCallEnded(1, + "+0987654321", + "+1234567890", + true, + 500, + true, + false); + + equal_received_system_message("telephony-call-ended", { + serviceId: 1, + number: "+0987654321", + emergency: true, + duration: 500, + direction: "outgoing", + hangUpLocal: false, + secondNumber: "+1234567890" + }); + + run_next_test(); +}); + +/** + * Verify RILSystemMessenger.notifySms() + */ +add_test(function test_sms_messenger_notify_sms() { + let messenger = newRILSystemMessenger(); + let timestamp = Date.now(); + let sentTimestamp = timestamp + 100; + let deliveryTimestamp = sentTimestamp + 100; + + // Verify 'sms-received' system message. + messenger.notifySms(Ci.nsISmsMessenger.NOTIFICATION_TYPE_RECEIVED, + 1, + 2, + "99887766554433221100", + Ci.nsISmsService.DELIVERY_TYPE_RECEIVED, + Ci.nsISmsService.DELIVERY_STATUS_TYPE_SUCCESS, + "+0987654321", + null, + "Incoming message", + Ci.nsISmsService.MESSAGE_CLASS_TYPE_CLASS_2, + timestamp, + sentTimestamp, + 0, + false); + + equal_received_system_message("sms-received", { + iccId: "99887766554433221100", + type: "sms", + id: 1, + threadId: 2, + delivery: "received", + deliveryStatus: "success", + sender: "+0987654321", + receiver: null, + body: "Incoming message", + messageClass: "class-2", + timestamp: timestamp, + sentTimestamp: sentTimestamp, + deliveryTimestamp: 0, + read: false + }); + + // Verify 'sms-sent' system message. + messenger.notifySms(Ci.nsISmsMessenger.NOTIFICATION_TYPE_SENT, + 3, + 4, + "99887766554433221100", + Ci.nsISmsService.DELIVERY_TYPE_SENT, + Ci.nsISmsService.DELIVERY_STATUS_TYPE_PENDING, + null, + "+0987654321", + "Outgoing message", + Ci.nsISmsService.MESSAGE_CLASS_TYPE_NORMAL, + timestamp, + 0, + 0, + true); + + equal_received_system_message("sms-sent", { + iccId: "99887766554433221100", + type: "sms", + id: 3, + threadId: 4, + delivery: "sent", + deliveryStatus: "pending", + sender: null, + receiver: "+0987654321", + body: "Outgoing message", + messageClass: "normal", + timestamp: timestamp, + sentTimestamp: 0, + deliveryTimestamp: 0, + read: true + }); + + // Verify 'sms-delivery-success' system message. + messenger.notifySms(Ci.nsISmsMessenger.NOTIFICATION_TYPE_DELIVERY_SUCCESS, + 5, + 6, + "99887766554433221100", + Ci.nsISmsService.DELIVERY_TYPE_SENT, + Ci.nsISmsService.DELIVERY_STATUS_TYPE_SUCCESS, + null, + "+0987654321", + "Outgoing message", + Ci.nsISmsService.MESSAGE_CLASS_TYPE_NORMAL, + timestamp, + 0, + deliveryTimestamp, + true); + + equal_received_system_message("sms-delivery-success", { + iccId: "99887766554433221100", + type: "sms", + id: 5, + threadId: 6, + delivery: "sent", + deliveryStatus: "success", + sender: null, + receiver: "+0987654321", + body: "Outgoing message", + messageClass: "normal", + timestamp: timestamp, + sentTimestamp: 0, + deliveryTimestamp: deliveryTimestamp, + read: true + }); + + // Verify 'sms-failed' system message. + messenger.notifySms(Ci.nsISmsMessenger.NOTIFICATION_TYPE_SENT_FAILED, + 7, + 8, + "99887766554433221100", + Ci.nsISmsService.DELIVERY_TYPE_ERROR, + Ci.nsISmsService.DELIVERY_STATUS_TYPE_ERROR, + null, + "+0987654321", + "Outgoing message", + Ci.nsISmsService.MESSAGE_CLASS_TYPE_NORMAL, + timestamp, + 0, + 0, + true); + + equal_received_system_message("sms-failed", { + iccId: "99887766554433221100", + type: "sms", + id: 7, + threadId: 8, + delivery: "error", + deliveryStatus: "error", + sender: null, + receiver: "+0987654321", + body: "Outgoing message", + messageClass: "normal", + timestamp: timestamp, + sentTimestamp: 0, + deliveryTimestamp: 0, + read: true + }); + + // Verify 'sms-delivery-error' system message. + messenger.notifySms(Ci.nsISmsMessenger.NOTIFICATION_TYPE_DELIVERY_ERROR, + 9, + 10, + "99887766554433221100", + Ci.nsISmsService.DELIVERY_TYPE_SENT, + Ci.nsISmsService.DELIVERY_STATUS_TYPE_ERROR, + null, + "+0987654321", + "Outgoing message", + Ci.nsISmsService.MESSAGE_CLASS_TYPE_NORMAL, + timestamp, + 0, + 0, + true); + + equal_received_system_message("sms-delivery-error", { + iccId: "99887766554433221100", + type: "sms", + id: 9, + threadId: 10, + delivery: "sent", + deliveryStatus: "error", + sender: null, + receiver: "+0987654321", + body: "Outgoing message", + messageClass: "normal", + timestamp: timestamp, + sentTimestamp: 0, + deliveryTimestamp: 0, + read: true + }); + + // Verify the protection of invalid nsISmsMessenger.NOTIFICATION_TYPEs. + try { + messenger.notifySms(5, + 1, + 2, + "99887766554433221100", + Ci.nsISmsService.DELIVERY_TYPE_RECEIVED, + Ci.nsISmsService.DELIVERY_STATUS_TYPE_SUCCESS, + "+0987654321", + null, + "Incoming message", + Ci.nsISmsService.MESSAGE_CLASS_TYPE_NORMAL, + timestamp, + sentTimestamp, + 0, + false); + ok(false, "Failed to verify the protection of invalid nsISmsMessenger.NOTIFICATION_TYPE!"); + } catch (e) {} + + run_next_test(); +}); + +/** + * Verify RILSystemMessenger.notifyCbMessageReceived() + */ +add_test(function test_cellbroadcast_messenger_notify_cb_message_received() { + let messenger = newRILSystemMessenger(); + let timestamp = Date.now(); + + // Verify ETWS + messenger.notifyCbMessageReceived(0, + Ci.nsICellBroadcastService.GSM_GEOGRAPHICAL_SCOPE_CELL_IMMEDIATE, + 256, + 4352, + null, + null, + Ci.nsICellBroadcastService.GSM_MESSAGE_CLASS_NORMAL, + timestamp, + Ci.nsICellBroadcastService.CDMA_SERVICE_CATEGORY_INVALID, + true, + Ci.nsICellBroadcastService.GSM_ETWS_WARNING_EARTHQUAKE, + false, + true); + equal_received_system_message("cellbroadcast-received", { + serviceId: 0, + gsmGeographicalScope: "cell-immediate", + messageCode: 256, + messageId: 4352, + language: null, + body: null, + messageClass: "normal", + timestamp: timestamp, + cdmaServiceCategory: null, + etws: { + warningType: "earthquake", + emergencyUserAlert: false, + popup: true + } + }); + + // Verify Normal CB Message + messenger.notifyCbMessageReceived(1, + Ci.nsICellBroadcastService.GSM_GEOGRAPHICAL_SCOPE_PLMN, + 0, + 50, + "en", + "The quick brown fox jumps over the lazy dog", + Ci.nsICellBroadcastService.GSM_MESSAGE_CLASS_NORMAL, + timestamp, + Ci.nsICellBroadcastService.CDMA_SERVICE_CATEGORY_INVALID, + false, + Ci.nsICellBroadcastService.GSM_ETWS_WARNING_INVALID, + false, + false); + equal_received_system_message("cellbroadcast-received", { + serviceId: 1, + gsmGeographicalScope: "plmn", + messageCode: 0, + messageId: 50, + language: "en", + body: "The quick brown fox jumps over the lazy dog", + messageClass: "normal", + timestamp: timestamp, + cdmaServiceCategory: null, + etws: null + }); + + // Verify CB Message with ETWS Info + messenger.notifyCbMessageReceived(0, + Ci.nsICellBroadcastService.GSM_GEOGRAPHICAL_SCOPE_LOCATION_AREA, + 0, + 4354, + "en", + "Earthquake & Tsunami Warning!", + Ci.nsICellBroadcastService.GSM_MESSAGE_CLASS_0, + timestamp, + Ci.nsICellBroadcastService.CDMA_SERVICE_CATEGORY_INVALID, + true, + Ci.nsICellBroadcastService.GSM_ETWS_WARNING_EARTHQUAKE_TSUNAMI, + true, + false); + equal_received_system_message("cellbroadcast-received", { + serviceId: 0, + gsmGeographicalScope: "location-area", + messageCode: 0, + messageId: 4354, + language: "en", + body: "Earthquake & Tsunami Warning!", + messageClass: "class-0", + timestamp: timestamp, + cdmaServiceCategory: null, + etws: { + warningType: "earthquake-tsunami", + emergencyUserAlert: true, + popup: false + } + }); + + // Verify CDMA CB Message + messenger.notifyCbMessageReceived(0, + Ci.nsICellBroadcastService.GSM_GEOGRAPHICAL_SCOPE_INVALID, + 0, + 0, + null, + "CDMA CB Message", + Ci.nsICellBroadcastService.GSM_MESSAGE_CLASS_NORMAL, + timestamp, + 512, + false, + Ci.nsICellBroadcastService.GSM_ETWS_WARNING_INVALID, + false, + false); + equal_received_system_message("cellbroadcast-received", { + serviceId: 0, + gsmGeographicalScope: null, + messageCode: 0, + messageId: 0, + language: null, + body: "CDMA CB Message", + messageClass: "normal", + timestamp: timestamp, + cdmaServiceCategory: 512, + etws: null + }); + + run_next_test(); +}); + +/** + * Verify RILSystemMessenger.notifyUssdReceived() + */ +add_test(function test_mobileconnection_notify_ussd_received() { + let messenger = newRILSystemMessenger(); + + messenger.notifyUssdReceived(0, "USSD Message", false); + + equal_received_system_message("ussd-received", { + serviceId: 0, + message: "USSD Message", + sessionEnded: false + }); + + messenger.notifyUssdReceived(1, "USSD Message", true); + + equal_received_system_message("ussd-received", { + serviceId: 1, + message: "USSD Message", + sessionEnded: true + }); + + run_next_test(); +}); + +/** + * Verify RILSystemMessenger.notifyCdmaInfoRecXXX() + */ +add_test(function test_mobileconnection_notify_cdma_info() { + let messenger = newRILSystemMessenger(); + + messenger.notifyCdmaInfoRecDisplay(0, "CDMA Display Info"); + + equal_received_system_message("cdma-info-rec-received", { + clientId: 0, + display: "CDMA Display Info" + }); + + messenger.notifyCdmaInfoRecCalledPartyNumber(1, 1, 2, "+0987654321", 3, 4); + + equal_received_system_message("cdma-info-rec-received", { + clientId: 1, + calledNumber: { + type: 1, + plan: 2, + number: "+0987654321", + pi: 3, + si: 4 + } + }); + + messenger.notifyCdmaInfoRecCallingPartyNumber(0, 5, 6, "+1234567890", 7, 8); + + equal_received_system_message("cdma-info-rec-received", { + clientId: 0, + callingNumber: { + type: 5, + plan: 6, + number: "+1234567890", + pi: 7, + si: 8 + } + }); + + messenger.notifyCdmaInfoRecConnectedPartyNumber(1, 4, 3, "+56473839201", 2, 1); + + equal_received_system_message("cdma-info-rec-received", { + clientId: 1, + connectedNumber: { + type: 4, + plan: 3, + number: "+56473839201", + pi: 2, + si: 1 + } + }); + + messenger.notifyCdmaInfoRecSignal(0, 1, 2, 3); + + equal_received_system_message("cdma-info-rec-received", { + clientId: 0, + signal: { + type: 1, + alertPitch: 2, + signal: 3 + } + }); + + messenger.notifyCdmaInfoRecRedirectingNumber(1, 8, 7, "+1029384756", 6, 5, 4); + + equal_received_system_message("cdma-info-rec-received", { + clientId: 1, + redirect: { + type: 8, + plan: 7, + number: "+1029384756", + pi: 6, + si: 5, + reason: 4 + } + }); + + messenger.notifyCdmaInfoRecLineControl(0, 1, 0, 1, 255); + + equal_received_system_message("cdma-info-rec-received", { + clientId: 0, + lineControl: { + polarityIncluded: 1, + toggle: 0, + reverse: 1, + powerDenial: 255 + } + }); + + messenger.notifyCdmaInfoRecClir(1, 256); + + equal_received_system_message("cdma-info-rec-received", { + clientId: 1, + clirCause: 256 + }); + + messenger.notifyCdmaInfoRecAudioControl(0, 255, -1); + + equal_received_system_message("cdma-info-rec-received", { + clientId: 0, + audioControl: { + upLink: 255, + downLink: -1 + } + }); + + run_next_test(); +}); + +/** + * Verify Error Handling of StkProactiveCmdFactory.createCommand() + */ +add_test(function test_icc_stk_cmd_factory_create_command_error() { + let messenger = newRILSystemMessenger(); + + // Verify the protection of invalid typeOfCommand. + try { + gStkCmdFactory.createCommand({ + commandNumber: 0, + typeOfCommand: RIL.STK_CMD_MORE_TIME, // Invalid TypeOfCommand + commandQualifier: 0x00 + }); + + ok(false, "Failed to verify the protection of createCommand()!"); + } catch (e) { + ok(e.message.indexOf("Unknown Command Type") !== -1, + "Invalid typeOfCommand!"); + } + + run_next_test(); +}); + +/** + * Verify Error Handling of StkProactiveCmdFactory.createCommandMessage() + */ +add_test(function test_icc_stk_cmd_factory_create_system_msg_invalid_cmd_type() { + let messenger = newRILSystemMessenger(); + let iccId = "99887766554433221100"; + + // Verify the protection of invalid typeOfCommand. + try { + gStkCmdFactory.createCommandMessage({ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIStkProactiveCmd]), + + // nsIStkProactiveCmd + commandNumber: 0, + typeOfCommand: RIL.STK_CMD_MORE_TIME, // Invalid TypeOfCommand + commandQualifier: 0 + }); + + ok(false, "Failed to identify invalid typeOfCommand!"); + } catch (e) { + ok(e.message.indexOf("Unknown Command Type") !== -1, + "Invalid typeOfCommand!"); + } + + run_next_test(); +}); + +/** + * Verify Error Handling of StkProactiveCmdFactory.createCommandMessage() + */ +add_test(function test_icc_stk_cmd_factory_create_system_msg_incorrect_cmd_type() { + let messenger = newRILSystemMessenger(); + let iccId = "99887766554433221100"; + + // Verify the protection of invalid typeOfCommand. + try { + gStkCmdFactory.createCommandMessage({ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIStkProactiveCmd, + Ci.nsIStkProvideLocalInfoCmd]), + + // nsIStkProactiveCmd + commandNumber: 0, + typeOfCommand: RIL.STK_CMD_POLL_INTERVAL, // Incorrect typeOfCommand + commandQualifier: 0, + // nsIStkProvideLocalInfoCmd + localInfoType: 0x00, + }); + + ok(false, "Failed to identify incorrect typeOfCommand!"); + } catch (e) { + ok(e.message.indexOf("Failed to convert command into concrete class: ") !== -1); + } + + run_next_test(); +}); + +/** + * Verify RILSystemMessenger.notifyStkProactiveCommand() + */ +add_test(function test_icc_notify_stk_proactive_command() { + let messenger = newRILSystemMessenger(); + let iccId = "99887766554433221100"; + let WHT = 0xFFFFFFFF; + let BLK = 0x000000FF; + let RED = 0xFF0000FF; + let GRN = 0x00FF00FF; + let BLU = 0x0000FFFF; + let TSP = 0; + // Basic Image, see Anex B.1 in TS 31.102. + let basicIcon = { + width: 8, + height: 8, + codingScheme: "basic", + pixels: [WHT, WHT, WHT, WHT, WHT, WHT, WHT, WHT, + BLK, BLK, BLK, BLK, BLK, BLK, WHT, WHT, + WHT, BLK, WHT, BLK, BLK, WHT, BLK, WHT, + WHT, BLK, BLK, WHT, WHT, BLK, BLK, WHT, + WHT, BLK, BLK, WHT, WHT, BLK, BLK, WHT, + WHT, BLK, WHT, BLK, BLK, WHT, BLK, WHT, + WHT, WHT, BLK, BLK, BLK, BLK, WHT, WHT, + WHT, WHT, WHT, WHT, WHT, WHT, WHT, WHT] + }; + // Color Image, see Anex B.2 in TS 31.102. + let colorIcon = { + width: 8, + height: 8, + codingScheme: "color", + pixels: [BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU, + BLU, RED, RED, RED, RED, RED, RED, BLU, + BLU, RED, GRN, GRN, GRN, RED, RED, BLU, + BLU, RED, RED, GRN, GRN, RED, RED, BLU, + BLU, RED, RED, GRN, GRN, RED, RED, BLU, + BLU, RED, RED, GRN, GRN, GRN, RED, BLU, + BLU, RED, RED, RED, RED, RED, RED, BLU, + BLU, BLU, BLU, BLU, BLU, BLU, BLU, BLU] + }; + // Color Image with Transparency, see Anex B.2 in TS 31.102. + let colorTransparencyIcon = { + width: 8, + height: 8, + codingScheme: "color-transparency", + pixels: [TSP, TSP, TSP, TSP, TSP, TSP, TSP, TSP, + TSP, RED, RED, RED, RED, RED, RED, TSP, + TSP, RED, GRN, GRN, GRN, RED, RED, TSP, + TSP, RED, RED, GRN, GRN, RED, RED, TSP, + TSP, RED, RED, GRN, GRN, RED, RED, TSP, + TSP, RED, RED, GRN, GRN, GRN, RED, TSP, + TSP, RED, RED, RED, RED, RED, RED, TSP, + TSP, TSP, TSP, TSP, TSP, TSP, TSP, TSP] + }; + + let cmdCount = 0; + + // Test Messages: + let messages = [ + // STK_CMD_REFRESH + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_REFRESH, + commandQualifier: 0x04 // UICC Reset + }, + // STK_CMD_POLL_INTERVAL + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_POLL_INTERVAL, + commandQualifier: 0x00, // RFU + options: { + timeUnit: RIL.STK_TIME_UNIT_TENTH_SECOND, + timeInterval: 0x05 + } + }, + // STK_CMD_POLL_OFF + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_POLL_OFF, + commandQualifier: 0x00, // RFU + }, + // STK_CMD_PROVIDE_LOCAL_INFO + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_PROVIDE_LOCAL_INFO, + commandQualifier: 0x01, // IMEI of the terminal + options: { + localInfoType: 0x01 // IMEI of the terminal + } + }, + // STK_CMD_SET_UP_EVENT_LIST with eventList + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SET_UP_EVENT_LIST, + commandQualifier: 0x00, // RFU + options: { + eventList: [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C ] + } + }, + // STK_CMD_SET_UP_EVENT_LIST without eventList + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SET_UP_EVENT_LIST, + commandQualifier: 0x00, // RFU + options: { + eventList: null + } + }, + // STK_CMD_SET_UP_MENU with mandatory properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SET_UP_MENU, + commandQualifier: 0x80, // bit 8: 1 = help information available + options: { + title: "Toolkit Menu 1", + items: [ + { identifier: 0x01, text: "Menu Item 1" }, + { identifier: 0x02, text: "Menu Item 2" }, + { identifier: 0x03, text: "Menu Item 3" } + ], + isHelpAvailable: true + } + }, + // STK_CMD_SET_UP_MENU with optional properties including: + // iconInfo for this menu, iconInfo for each item and nextActionList. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SET_UP_MENU, + commandQualifier: 0x00, // bit 8: 0 = help information is not available + options: { + title: "Toolkit Menu 2", + items: [ + { identifier: 0x01, + text: "Menu Item 1", + iconSelfExplanatory: true, + icons: [basicIcon] + }, + { identifier: 0x02, + text: "Menu Item 2", + iconSelfExplanatory: false, + icons: [basicIcon, colorIcon] + }, + { identifier: 0x03, + text: "Menu Item 3", + iconSelfExplanatory: true, + icons: [basicIcon, colorIcon, colorTransparencyIcon] + }, + ], + nextActionList: [ + RIL.STK_NEXT_ACTION_END_PROACTIVE_SESSION, + RIL.STK_NEXT_ACTION_NULL, + RIL.STK_NEXT_ACTION_NULL, + RIL.STK_NEXT_ACTION_NULL + ], + iconSelfExplanatory: false, + icons: [basicIcon, colorIcon, colorTransparencyIcon], + isHelpAvailable: false + } + }, + // STK_CMD_SELECT_ITEM with mandatory properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SELECT_ITEM, + commandQualifier: RIL.STK_PRESENTATION_TYPE_NOT_SPECIFIED, + options: { + title: null, + items: [ + { identifier: 0x01, text: "Menu Item 1" }, + { identifier: 0x02, text: "Menu Item 2" }, + { identifier: 0x03, text: "Menu Item 3" } + ], + presentationType: RIL.STK_PRESENTATION_TYPE_NOT_SPECIFIED, + isHelpAvailable: false + } + }, + // STK_CMD_SELECT_ITEM with optional properties including: + // title, iconInfo for this menu, iconInfo for each item and nextActionList. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SELECT_ITEM, + commandQualifier: RIL.STK_PRESENTATION_TYPE_NAVIGATION_OPTIONS, + options: { + title: "Selected Toolkit Menu", + items: [ + { identifier: 0x01, + text: "Menu Item 1", + iconSelfExplanatory: true, + icons: [basicIcon] + }, + { identifier: 0x02, + text: "Menu Item 2", + iconSelfExplanatory: false, + icons: [basicIcon, colorIcon] + }, + { identifier: 0x03, + text: "Menu Item 3", + iconSelfExplanatory: true, + icons: [basicIcon, colorIcon, colorTransparencyIcon] + }, + ], + nextActionList: [ + RIL.STK_NEXT_ACTION_END_PROACTIVE_SESSION, + RIL.STK_NEXT_ACTION_NULL, + RIL.STK_NEXT_ACTION_NULL, + RIL.STK_NEXT_ACTION_NULL + ], + defaultItem: 0x02, + iconSelfExplanatory: false, + icons: [basicIcon, colorIcon, colorTransparencyIcon], + presentationType: RIL.STK_PRESENTATION_TYPE_NAVIGATION_OPTIONS, + isHelpAvailable: false + } + }, + // STK_CMD_DISPLAY_TEXT with mandatory properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_DISPLAY_TEXT, + commandQualifier: 0x01, // bit 1: High Priority + options: { + text: "Display Text 1", + isHighPriority: true, + userClear: false, + responseNeeded: false + } + }, + // STK_CMD_DISPLAY_TEXT with optional properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_DISPLAY_TEXT, + commandQualifier: 0x80, // bit 8: User Clear + options: { + text: "Display Text 2", + isHighPriority: false, + userClear: true, + responseNeeded: true, + duration: { + timeUnit: RIL.STK_TIME_UNIT_TENTH_SECOND, + timeInterval: 0x05 + }, + iconSelfExplanatory: true, + icons: [basicIcon] + } + }, + // STK_CMD_SET_UP_IDLE_MODE_TEXT + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SET_UP_IDLE_MODE_TEXT, + commandQualifier: 0x00, // RFU + options: { + text: "Setup Idle Mode Text" + } + }, + // STK_CMD_SEND_SS + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SEND_SS, + commandQualifier: 0x00, // RFU + options: { + text: "Send SS", + iconSelfExplanatory: true, + icons: [colorIcon] + } + }, + // STK_CMD_SEND_USSD + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SEND_USSD, + commandQualifier: 0x00, // RFU + options: { + text: "Send USSD" + } + }, + // STK_CMD_SEND_SMS + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SEND_SMS, + commandQualifier: 0x00, // RFU + options: { + text: "Send SMS", + iconSelfExplanatory: false, + icons: [colorTransparencyIcon] + } + }, + // STK_CMD_SEND_DTMF + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SEND_DTMF, + commandQualifier: 0x00, // RFU + options: { + text: "Send DTMF", + iconSelfExplanatory: true, + icons: [basicIcon] + } + }, + // STK_CMD_GET_INKEY + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_GET_INKEY, + commandQualifier: 0x84, // bit 3: isYesNoRequested, bit 8: isHelpAvailable + options: { + text: "Get Input Key", + minLength: 1, + maxLength: 1, + duration: { + timeUnit: RIL.STK_TIME_UNIT_SECOND, + timeInterval: 0x0A + }, + isAlphabet: false, + isUCS2: false, + isYesNoRequested: true, + isHelpAvailable: true, + defaultText: null, + iconSelfExplanatory: false, + icons: [colorIcon] + } + }, + // STK_CMD_GET_INPUT + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_GET_INPUT, + commandQualifier: 0x0F, // bit 1-4: isAlphabet, isUCS2, hideInput, isPacked + options: { + text: "Get Input Text", + minLength: 1, + maxLength: 255, + defaultText: "Default Input Text", + isAlphabet: true, + isUCS2: true, + hideInput: true, + isPacked: true, + isHelpAvailable: false, + defaultText: null, + iconSelfExplanatory: true, + icons: [basicIcon] + } + }, + // STK_CMD_SET_UP_CALL with mandatory properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SET_UP_CALL, + commandQualifier: 0x00, // RFU + options: { + address: "+0987654321" + } + }, + // STK_CMD_SET_UP_CALL with optional properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SET_UP_CALL, + commandQualifier: 0x00, // RFU + options: { + address: "+0987654321", + confirmMessage: { + text: "Confirm Message", + iconSelfExplanatory: false, + icons: [colorIcon] + }, + callMessage: { + text: "Call Message", + iconSelfExplanatory: true, + icons: [basicIcon] + }, + duration: { + timeUnit: RIL.STK_TIME_UNIT_SECOND, + timeInterval: 0x0A + } + } + }, + // STK_CMD_LAUNCH_BROWSER with mandatory properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_LAUNCH_BROWSER, + commandQualifier: RIL.STK_BROWSER_MODE_USING_NEW_BROWSER, + options: { + url: "http://www.mozilla.org", + mode: RIL.STK_BROWSER_MODE_USING_NEW_BROWSER + } + }, + // STK_CMD_LAUNCH_BROWSER with optional properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_LAUNCH_BROWSER, + commandQualifier: RIL.STK_BROWSER_MODE_USING_NEW_BROWSER, + options: { + url: "http://www.mozilla.org", + mode: RIL.STK_BROWSER_MODE_USING_NEW_BROWSER, + confirmMessage: { + text: "Confirm Message for Launch Browser", + iconSelfExplanatory: false, + icons: [colorTransparencyIcon] + } + } + }, + // STK_CMD_PLAY_TONE with mandatory properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_PLAY_TONE, + commandQualifier: 0x01, // isVibrate + options: { + text: null, + isVibrate: true + } + }, + // STK_CMD_PLAY_TONE with optional properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_PLAY_TONE, + commandQualifier: 0x00, // isVibrate = false + options: { + text: "Play Tone", + tone: RIL.STK_TONE_TYPE_CONGESTION, + isVibrate: false, + duration: { + timeUnit: RIL.STK_TIME_UNIT_SECOND, + timeInterval: 0x0A + }, + iconSelfExplanatory: true, + icons: [basicIcon] + } + }, + // STK_CMD_TIMER_MANAGEMENT with mandatory properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_TIMER_MANAGEMENT, + commandQualifier: RIL.STK_TIMER_DEACTIVATE, + options: { + timerId: 0x08, + timerAction: RIL.STK_TIMER_DEACTIVATE + } + }, + // STK_CMD_TIMER_MANAGEMENT with optional properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_TIMER_MANAGEMENT, + commandQualifier: RIL.STK_TIMER_START, + options: { + timerId: 0x01, + timerValue: (12 * 60 * 60) + (30 * 60) + (30), // 12:30:30 + timerAction: RIL.STK_TIMER_START + } + }, + // STK_CMD_OPEN_CHANNEL with mandatory properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_OPEN_CHANNEL, + commandQualifier: 0x00, //RFU + options: { + text: null, + } + }, + // STK_CMD_OPEN_CHANNEL with optional properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_OPEN_CHANNEL, + commandQualifier: 0x00, //RFU + options: { + text: "Open Channel", + iconSelfExplanatory: false, + icons: [colorIcon] + } + }, + // STK_CMD_CLOSE_CHANNEL with optional properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_CLOSE_CHANNEL, + commandQualifier: 0x00, //RFU + options: { + text: "Close Channel", + iconSelfExplanatory: true, + icons: [colorTransparencyIcon] + } + }, + // STK_CMD_SEND_DATA with optional properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_SEND_DATA, + commandQualifier: 0x00, //RFU + options: { + text: null, + iconSelfExplanatory: false, + icons: [basicIcon] + } + }, + // STK_CMD_RECEIVE_DATA with optional properties. + { + commandNumber: ++cmdCount, + typeOfCommand: RIL.STK_CMD_RECEIVE_DATA, + commandQualifier: 0x00, //RFU + options: { + text: "Receive Data" + } + }, + null // Termination condition to run_next_test() + ]; + + messages.forEach(function(aMessage) { + if (!aMessage) { + run_next_test(); + return; + } + + messenger.notifyStkProactiveCommand(iccId, + gStkCmdFactory.createCommand(aMessage)); + + equal_received_system_message("icc-stkcommand", { + iccId: iccId, + command: aMessage + }); + }); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_barring_password.js b/dom/system/gonk/tests/test_ril_worker_barring_password.js new file mode 100644 index 000000000..fcd3e4405 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_barring_password.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +const PIN = "0000"; +const NEW_PIN = "1234"; + +add_test(function test_change_call_barring_password() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + + function do_test(facility, pin, newPin) { + buf.sendParcel = function fakeSendParcel () { + // Request Type. + equal(this.readInt32(), REQUEST_CHANGE_BARRING_PASSWORD); + + // Token : we don't care. + this.readInt32(); + + let parcel = this.readStringList(); + equal(parcel.length, 3); + equal(parcel[0], facility); + equal(parcel[1], pin); + equal(parcel[2], newPin); + }; + + let options = {facility: facility, pin: pin, newPin: newPin}; + context.RIL.changeCallBarringPassword(options); + } + + do_test(ICC_CB_FACILITY_BA_ALL, PIN, NEW_PIN); + + run_next_test(); +}); + +add_test(function test_check_change_call_barring_password_result() { + let barringPasswordOptions; + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + + let context = worker.ContextPool._contexts[0]; + context.RIL.changeCallBarringPassword = + function fakeChangeCallBarringPassword(options) { + barringPasswordOptions = options; + context.RIL[REQUEST_CHANGE_BARRING_PASSWORD](0, {}); + }; + + context.RIL.changeCallBarringPassword({pin: PIN, newPin: NEW_PIN}); + + let postedMessage = workerHelper.postedMessage; + equal(barringPasswordOptions.pin, PIN); + equal(barringPasswordOptions.newPin, NEW_PIN); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_buf.js b/dom/system/gonk/tests/test_ril_worker_buf.js new file mode 100644 index 000000000..30054a881 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_buf.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + run_next_test(); +} + +/** + * Add test function with specified parcel and request handler. + * + * @param parcel + * Incoming parcel to be tested. + * @param handler + * Handler to be invoked as RIL request handler. + */ +function add_test_incoming_parcel(parcel, handler) { + add_test(function test_incoming_parcel() { + let worker = newWorker({ + postRILMessage: function(data) { + // do nothing + }, + postMessage: function(message) { + // do nothing + } + }); + + if (!parcel) { + parcel = newIncomingParcel(-1, + worker.RESPONSE_TYPE_UNSOLICITED, + worker.REQUEST_VOICE_REGISTRATION_STATE, + [0, 0, 0, 0]); + } + + let context = worker.ContextPool._contexts[0]; + // supports only requests less or equal than UINT8_MAX(255). + let buf = context.Buf; + let request = parcel[buf.PARCEL_SIZE_SIZE + buf.UINT32_SIZE]; + context.RIL[request] = function ril_request_handler() { + handler.apply(this, arguments); + }; + + worker.onRILMessage(0, parcel); + + // end of incoming parcel's trip, let's do next test. + run_next_test(); + }); +} + +// Test normal parcel handling. +add_test_incoming_parcel(null, + function test_normal_parcel_handling() { + let self = this; + try { + // reads exactly the same size, should not throw anything. + self.context.Buf.readInt32(); + } catch (e) { + ok(false, "Got exception: " + e); + } + } +); + +// Test parcel under read. +add_test_incoming_parcel(null, + function test_parcel_under_read() { + let self = this; + try { + // reads less than parcel size, should not throw. + self.context.Buf.readUint16(); + } catch (e) { + ok(false, "Got exception: " + e); + } + } +); + +// Test parcel over read. +add_test_incoming_parcel(null, + function test_parcel_over_read() { + let buf = this.context.Buf; + + // read all data available + while (buf.readAvailable > 0) { + buf.readUint8(); + } + + throws(function over_read_handler() { + // reads more than parcel size, should throw an error. + buf.readUint8(); + },"Trying to read data beyond the parcel end!"); + } +); + +// Test Bug 814761: buffer overwritten +add_test(function test_incoming_parcel_buffer_overwritten() { + let worker = newWorker({ + postRILMessage: function(data) { + // do nothing + }, + postMessage: function(message) { + // do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + // A convenient alias. + let buf = context.Buf; + + // Allocate an array of specified size and set each of its elements to value. + function calloc(length, value) { + let array = new Array(length); + for (let i = 0; i < length; i++) { + array[i] = value; + } + return array; + } + + // Do nothing in handleParcel(). + let request = worker.REQUEST_VOICE_REGISTRATION_STATE; + context.RIL[request] = null; + + // Prepare two parcels, whose sizes are both smaller than the incoming buffer + // size but larger when combined, to trigger the bug. + let pA_dataLength = buf.incomingBufferLength / 2; + let pA = newIncomingParcel(-1, + worker.RESPONSE_TYPE_UNSOLICITED, + request, + calloc(pA_dataLength, 1)); + let pA_parcelSize = pA.length - buf.PARCEL_SIZE_SIZE; + + let pB_dataLength = buf.incomingBufferLength * 3 / 4; + let pB = newIncomingParcel(-1, + worker.RESPONSE_TYPE_UNSOLICITED, + request, + calloc(pB_dataLength, 1)); + let pB_parcelSize = pB.length - buf.PARCEL_SIZE_SIZE; + + // First, send an incomplete pA and verifies related data pointer: + let p1 = pA.subarray(0, pA.length - 1); + worker.onRILMessage(0, p1); + // The parcel should not have been processed. + equal(buf.readAvailable, 0); + // buf.currentParcelSize should have been set because incoming data has more + // than 4 octets. + equal(buf.currentParcelSize, pA_parcelSize); + // buf.readIncoming should contains remaining unconsumed octets count. + equal(buf.readIncoming, p1.length - buf.PARCEL_SIZE_SIZE); + // buf.incomingWriteIndex should be ready to accept the last octet. + equal(buf.incomingWriteIndex, p1.length); + + // Second, send the last octet of pA and whole pB. The Buf should now expand + // to cover both pA & pB. + let p2 = new Uint8Array(1 + pB.length); + p2.set(pA.subarray(pA.length - 1), 0); + p2.set(pB, 1); + worker.onRILMessage(0, p2); + // The parcels should have been both consumed. + equal(buf.readAvailable, 0); + // No parcel data remains. + equal(buf.currentParcelSize, 0); + // No parcel data remains. + equal(buf.readIncoming, 0); + // The Buf should now expand to cover both pA & pB. + equal(buf.incomingWriteIndex, pA.length + pB.length); + + // end of incoming parcel's trip, let's do next test. + run_next_test(); +}); + +// Test Buf.readUint8Array. +add_test_incoming_parcel(null, + function test_buf_readUint8Array() { + let buf = this.context.Buf; + + let u8array = buf.readUint8Array(1); + equal(u8array instanceof Uint8Array, true); + equal(u8array.length, 1); + equal(buf.readAvailable, 3); + + u8array = buf.readUint8Array(2); + equal(u8array.length, 2); + equal(buf.readAvailable, 1); + + throws(function over_read_handler() { + // reads more than parcel size, should throw an error. + u8array = buf.readUint8Array(2); + }, "Trying to read data beyond the parcel end!"); + } +); diff --git a/dom/system/gonk/tests/test_ril_worker_cdma_info_rec.js b/dom/system/gonk/tests/test_ril_worker_cdma_info_rec.js new file mode 100644 index 000000000..335c0c403 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_cdma_info_rec.js @@ -0,0 +1,234 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Helper function. + */ +function newWorkerWithParcel(parcelBuf) { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let index = 0; // index for read + let buf = parcelBuf; + + let context = worker.ContextPool._contexts[0]; + context.Buf.readUint8 = function() { + return buf[index++]; + }; + + context.Buf.readUint16 = function() { + return buf[index++]; + }; + + context.Buf.readInt32 = function() { + return buf[index++]; + }; + + context.Buf.seekIncoming = function(offset) { + index += offset / context.Buf.UINT32_SIZE; + }; + + return worker; +} + +// Test CDMA information record decoder. + +/** + * Verify decoder for type DISPLAY + */ +add_test(function test_display() { + let worker = newWorkerWithParcel([ + 0x01, // one inforemation record + 0x00, // type: display + 0x09, // length: 9 + 0x54, 0x65, 0x73, 0x74, 0x20, 0x49, 0x6E, 0x66, + 0x6F, 0x00]); + let context = worker.ContextPool._contexts[0]; + let helper = context.CdmaPDUHelper; + let records = helper.decodeInformationRecord(); + + equal(records[0].display, "Test Info"); + + run_next_test(); +}); + +/** + * Verify decoder for type EXTENDED DISPLAY + */ +add_test(function test_extended_display() { + let worker = newWorkerWithParcel([ + 0x01, // one inforemation record + 0x07, // type: extended display + 0x12, // length: 18 + 0x54, 0x65, 0x73, 0x74, 0x20, 0x45, 0x78, 0x74, + 0x65, 0x6E, 0x64, 0x65, 0x64, 0x20, 0x49, 0x6E, + 0x66, 0x6F, 0x00, 0x00]); + let context = worker.ContextPool._contexts[0]; + let helper = context.CdmaPDUHelper; + let records = helper.decodeInformationRecord(); + + equal(records[0].display, "Test Extended Info"); + + run_next_test(); +}); + +/** + * Verify decoder for mixed type + */ +add_test(function test_mixed() { + let worker = newWorkerWithParcel([ + 0x02, // two inforemation record + 0x00, // type: display + 0x0B, // length: 11 + 0x54, 0x65, 0x73, 0x74, 0x20, 0x49, 0x6E, 0x66, + 0x6F, 0x20, 0x31, 0x00, + 0x07, // type: extended display + 0x0B, // length: 11 + 0x54, 0x65, 0x73, 0x74, 0x20, 0x49, 0x6E, 0x66, + 0x6F, 0x20, 0x32, 0x00]); + let context = worker.ContextPool._contexts[0]; + let helper = context.CdmaPDUHelper; + let records = helper.decodeInformationRecord(); + + equal(records[0].display, "Test Info 1"); + equal(records[1].display, "Test Info 2"); + + run_next_test(); +}); + +/** + * Verify decoder for multiple types + */ +add_test(function test_multiple() { + let worker = newWorkerWithParcel([ + 0x02, // two inforemation record + 0x00, // type: display + 0x0B, // length: 11 + 0x54, 0x65, 0x73, 0x74, 0x20, 0x49, 0x6E, 0x66, + 0x6F, 0x20, 0x31, 0x00, + 0x00, // type: display + 0x0B, // length: 11 + 0x54, 0x65, 0x73, 0x74, 0x20, 0x49, 0x6E, 0x66, + 0x6F, 0x20, 0x32, 0x00]); + let context = worker.ContextPool._contexts[0]; + let helper = context.CdmaPDUHelper; + let records = helper.decodeInformationRecord(); + + equal(records[0].display, "Test Info 1"); + equal(records[1].display, "Test Info 2"); + + run_next_test(); +}); + +/** + * Verify decoder for Signal Type + */ +add_test(function test_signal() { + let worker = newWorkerWithParcel([ + 0x01, // one inforemation record + 0x04, // type: signal + 0x01, // isPresent: non-zero + 0x00, // signalType: Tone signal (00) + 0x01, // alertPitch: High pitch + 0x03]); // signal: Abbreviated intercept (000011) + let context = worker.ContextPool._contexts[0]; + let helper = context.CdmaPDUHelper; + let records = helper.decodeInformationRecord(); + + equal(records[0].signal.type, 0x00); + equal(records[0].signal.alertPitch, 0x01); + equal(records[0].signal.signal, 0x03); + + run_next_test(); +}); + +/** + * Verify decoder for Signal Type for Not Presented + */ +add_test(function test_signal_not_present() { + let worker = newWorkerWithParcel([ + 0x01, // one inforemation record + 0x04, // type: signal + 0x00, // isPresent: zero + 0x00, // signalType: Tone signal (00) + 0x01, // alertPitch: High pitch + 0x03]); // signal: Abbreviated intercept (000011) + let context = worker.ContextPool._contexts[0]; + let helper = context.CdmaPDUHelper; + let records = helper.decodeInformationRecord(); + + equal(records.length, 0); + + run_next_test(); +}); + +/** + * Verify decoder for Line Control + */ +add_test(function test_line_control() { + let worker = newWorkerWithParcel([ + 0x01, // one inforemation record + 0x06, // type: line control + 0x01, // polarity included + 0x00, // not toggled + 0x01, // reversed + 0xFF]); // Power denial timeout: 255 * 5 ms + let context = worker.ContextPool._contexts[0]; + let helper = context.CdmaPDUHelper; + let records = helper.decodeInformationRecord(); + + equal(records[0].lineControl.polarityIncluded, 1); + equal(records[0].lineControl.toggle, 0); + equal(records[0].lineControl.reverse, 1); + equal(records[0].lineControl.powerDenial, 255); + + run_next_test(); +}); + +/** + * Verify decoder for CLIR Cause + */ +add_test(function test_clir() { + let worker = newWorkerWithParcel([ + 0x01, // one inforemation record + 0x08, // type: clir + 0x01]); // cause: Rejected by user + let context = worker.ContextPool._contexts[0]; + let helper = context.CdmaPDUHelper; + let records = helper.decodeInformationRecord(); + + equal(records[0].clirCause, 1); + + run_next_test(); +}); + +/** + * Verify decoder for Audio Control + */ +add_test(function test_clir() { + let worker = newWorkerWithParcel([ + 0x01, // one inforemation record + 0x0A, // type: audio control + 0x01, // uplink + 0xFF]); // downlink + let context = worker.ContextPool._contexts[0]; + let helper = context.CdmaPDUHelper; + let records = helper.decodeInformationRecord(); + + equal(records[0].audioControl.upLink, 1); + equal(records[0].audioControl.downLink, 255); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_cellbroadcast_config.js b/dom/system/gonk/tests/test_ril_worker_cellbroadcast_config.js new file mode 100644 index 000000000..d5645a3cf --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_cellbroadcast_config.js @@ -0,0 +1,470 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +add_test(function test_ril_worker_cellbroadcast_activate() { + let worker = newWorker({ + postRILMessage: function(id, parcel) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + let context = worker.ContextPool._contexts[0]; + + let parcelTypes = []; + let org_newParcel = context.Buf.newParcel; + context.Buf.newParcel = function(type, options) { + parcelTypes.push(type); + org_newParcel.apply(this, arguments); + }; + + function setup(isCdma) { + context.RIL._isCdma = isCdma; + context.RIL.cellBroadcastDisabled = false; + context.RIL.mergedCellBroadcastConfig = [1, 2, 4, 7]; // 1, 4-6 + parcelTypes = []; + } + + function test(isCdma, expectedRequest) { + setup(isCdma); + context.RIL.setCellBroadcastDisabled({disabled: true}); + // Makesure that request parcel is sent out. + notEqual(parcelTypes.indexOf(expectedRequest), -1); + equal(context.RIL.cellBroadcastDisabled, true); + } + + test(false, REQUEST_GSM_SMS_BROADCAST_ACTIVATION); + test(true, REQUEST_CDMA_SMS_BROADCAST_ACTIVATION); + + run_next_test(); +}); + +add_test(function test_ril_worker_cellbroadcast_config() { + let currentParcel; + let worker = newWorker({ + postRILMessage: function(id, parcel) { + currentParcel = parcel; + }, + postMessage: function(message) { + // Do nothing + } + }); + let context = worker.ContextPool._contexts[0]; + + function U32ArrayFromParcelArray(pa) { + do_print(pa); + let out = []; + for (let i = 0; i < pa.length; i += 4) { + let data = pa[i] + (pa[i+1] << 8) + (pa[i+2] << 16) + (pa[i+3] << 24); + out.push(data); + } + return out; + } + + function test(isCdma, configs, expected) { + let parcelType = isCdma ? REQUEST_CDMA_SET_BROADCAST_SMS_CONFIG + : REQUEST_GSM_SET_BROADCAST_SMS_CONFIG; + + let found = false; + worker.postRILMessage = function(id, parcel) { + let u32Parcel = U32ArrayFromParcelArray(Array.slice(parcel)); + if (u32Parcel[1] != parcelType) { + return; + } + + found = true; + // Check parcel. Data start from 4th word (32bit) + equal(u32Parcel.slice(3).toString(), expected); + }; + + context.RIL._isCdma = isCdma; + context.RIL.setSmsBroadcastConfig(configs); + + // Makesure that request parcel is sent out. + ok(found); + } + + // (GSM) RIL writes the following data to outgoing parcel: + // nums [(from, to, 0, 0xFF, 1), ... ] + test(false, + [1, 2, 4, 7] /* 1, 4-6 */, + ["2", "1,1,0,255,1", "4,6,0,255,1"].join()); + + // (CDMA) RIL writes the following data to outgoing parcel: + // nums [(id, 0, 1), ... ] + test(true, + [1, 2, 4, 7] /* 1, 4-6 */, + ["4", "1,0,1", "4,0,1", "5,0,1", "6,0,1"].join()); + + run_next_test(); +}); + +add_test(function test_ril_worker_cellbroadcast_merge_config() { + let worker = newWorker({ + postRILMessage: function(id, parcel) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + let context = worker.ContextPool._contexts[0]; + + function test(isCdma, configs, expected) { + context.RIL._isCdma = isCdma; + context.RIL.cellBroadcastConfigs = configs; + context.RIL._mergeAllCellBroadcastConfigs(); + equal(context.RIL.mergedCellBroadcastConfig.toString(), expected); + } + + let configs = { + MMI: [1, 2, 4, 7], // 1, 4-6 + CBMI: [6, 9], // 6-8 + CBMID: [8, 11], // 8-10 + CBMIR: [10, 13] // 10-12 + }; + + test(false, configs, "1,2,4,13"); + test(true, configs, "1,2,4,7"); + + run_next_test(); +}); + +add_test(function test_ril_worker_cellbroadcast_set_search_list() { + let worker = newWorker({ + postRILMessage: function(id, parcel) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + + function test(aIsCdma, aSearchList, aExpected) { + context.RIL._isCdma = aIsCdma; + + let options = { searchList: aSearchList }; + context.RIL.setCellBroadcastSearchList(options); + // Enforce the MMI result to string for comparison. + equal("" + context.RIL.cellBroadcastConfigs.MMI, aExpected); + do_check_eq(options.errorMsg, undefined); + } + + let searchListStr = "1,2,3,4"; + let searchList = { gsm: "1,2,3,4", cdma: "5,6,7,8" }; + + test(false, searchListStr, "1,2,2,3,3,4,4,5"); + test(true, searchListStr, "1,2,2,3,3,4,4,5"); + test(false, searchList, "1,2,2,3,3,4,4,5"); + test(true, searchList, "5,6,6,7,7,8,8,9"); + test(false, null, "null"); + test(true, null, "null"); + + run_next_test(); +}); + +add_test(function test_ril_worker_mergeCellBroadcastConfigs() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + + function test(olist, from, to, expected) { + let result = ril._mergeCellBroadcastConfigs(olist, from, to); + equal(JSON.stringify(expected), JSON.stringify(result)); + } + + test(null, 0, 1, [0, 1]); + + test([10, 13], 7, 8, [ 7, 8, 10, 13]); + test([10, 13], 7, 9, [ 7, 9, 10, 13]); + test([10, 13], 7, 10, [ 7, 13]); + test([10, 13], 7, 11, [ 7, 13]); + test([10, 13], 7, 12, [ 7, 13]); + test([10, 13], 7, 13, [ 7, 13]); + test([10, 13], 7, 14, [ 7, 14]); + test([10, 13], 7, 15, [ 7, 15]); + test([10, 13], 7, 16, [ 7, 16]); + test([10, 13], 8, 9, [ 8, 9, 10, 13]); + test([10, 13], 8, 10, [ 8, 13]); + test([10, 13], 8, 11, [ 8, 13]); + test([10, 13], 8, 12, [ 8, 13]); + test([10, 13], 8, 13, [ 8, 13]); + test([10, 13], 8, 14, [ 8, 14]); + test([10, 13], 8, 15, [ 8, 15]); + test([10, 13], 8, 16, [ 8, 16]); + test([10, 13], 9, 10, [ 9, 13]); + test([10, 13], 9, 11, [ 9, 13]); + test([10, 13], 9, 12, [ 9, 13]); + test([10, 13], 9, 13, [ 9, 13]); + test([10, 13], 9, 14, [ 9, 14]); + test([10, 13], 9, 15, [ 9, 15]); + test([10, 13], 9, 16, [ 9, 16]); + test([10, 13], 10, 11, [10, 13]); + test([10, 13], 10, 12, [10, 13]); + test([10, 13], 10, 13, [10, 13]); + test([10, 13], 10, 14, [10, 14]); + test([10, 13], 10, 15, [10, 15]); + test([10, 13], 10, 16, [10, 16]); + test([10, 13], 11, 12, [10, 13]); + test([10, 13], 11, 13, [10, 13]); + test([10, 13], 11, 14, [10, 14]); + test([10, 13], 11, 15, [10, 15]); + test([10, 13], 11, 16, [10, 16]); + test([10, 13], 12, 13, [10, 13]); + test([10, 13], 12, 14, [10, 14]); + test([10, 13], 12, 15, [10, 15]); + test([10, 13], 12, 16, [10, 16]); + test([10, 13], 13, 14, [10, 14]); + test([10, 13], 13, 15, [10, 15]); + test([10, 13], 13, 16, [10, 16]); + test([10, 13], 14, 15, [10, 13, 14, 15]); + test([10, 13], 14, 16, [10, 13, 14, 16]); + test([10, 13], 15, 16, [10, 13, 15, 16]); + + test([10, 13, 14, 17], 7, 8, [ 7, 8, 10, 13, 14, 17]); + test([10, 13, 14, 17], 7, 9, [ 7, 9, 10, 13, 14, 17]); + test([10, 13, 14, 17], 7, 10, [ 7, 13, 14, 17]); + test([10, 13, 14, 17], 7, 11, [ 7, 13, 14, 17]); + test([10, 13, 14, 17], 7, 12, [ 7, 13, 14, 17]); + test([10, 13, 14, 17], 7, 13, [ 7, 13, 14, 17]); + test([10, 13, 14, 17], 7, 14, [ 7, 17]); + test([10, 13, 14, 17], 7, 15, [ 7, 17]); + test([10, 13, 14, 17], 7, 16, [ 7, 17]); + test([10, 13, 14, 17], 7, 17, [ 7, 17]); + test([10, 13, 14, 17], 7, 18, [ 7, 18]); + test([10, 13, 14, 17], 7, 19, [ 7, 19]); + test([10, 13, 14, 17], 8, 9, [ 8, 9, 10, 13, 14, 17]); + test([10, 13, 14, 17], 8, 10, [ 8, 13, 14, 17]); + test([10, 13, 14, 17], 8, 11, [ 8, 13, 14, 17]); + test([10, 13, 14, 17], 8, 12, [ 8, 13, 14, 17]); + test([10, 13, 14, 17], 8, 13, [ 8, 13, 14, 17]); + test([10, 13, 14, 17], 8, 14, [ 8, 17]); + test([10, 13, 14, 17], 8, 15, [ 8, 17]); + test([10, 13, 14, 17], 8, 16, [ 8, 17]); + test([10, 13, 14, 17], 8, 17, [ 8, 17]); + test([10, 13, 14, 17], 8, 18, [ 8, 18]); + test([10, 13, 14, 17], 8, 19, [ 8, 19]); + test([10, 13, 14, 17], 9, 10, [ 9, 13, 14, 17]); + test([10, 13, 14, 17], 9, 11, [ 9, 13, 14, 17]); + test([10, 13, 14, 17], 9, 12, [ 9, 13, 14, 17]); + test([10, 13, 14, 17], 9, 13, [ 9, 13, 14, 17]); + test([10, 13, 14, 17], 9, 14, [ 9, 17]); + test([10, 13, 14, 17], 9, 15, [ 9, 17]); + test([10, 13, 14, 17], 9, 16, [ 9, 17]); + test([10, 13, 14, 17], 9, 17, [ 9, 17]); + test([10, 13, 14, 17], 9, 18, [ 9, 18]); + test([10, 13, 14, 17], 9, 19, [ 9, 19]); + test([10, 13, 14, 17], 10, 11, [10, 13, 14, 17]); + test([10, 13, 14, 17], 10, 12, [10, 13, 14, 17]); + test([10, 13, 14, 17], 10, 13, [10, 13, 14, 17]); + test([10, 13, 14, 17], 10, 14, [10, 17]); + test([10, 13, 14, 17], 10, 15, [10, 17]); + test([10, 13, 14, 17], 10, 16, [10, 17]); + test([10, 13, 14, 17], 10, 17, [10, 17]); + test([10, 13, 14, 17], 10, 18, [10, 18]); + test([10, 13, 14, 17], 10, 19, [10, 19]); + test([10, 13, 14, 17], 11, 12, [10, 13, 14, 17]); + test([10, 13, 14, 17], 11, 13, [10, 13, 14, 17]); + test([10, 13, 14, 17], 11, 14, [10, 17]); + test([10, 13, 14, 17], 11, 15, [10, 17]); + test([10, 13, 14, 17], 11, 16, [10, 17]); + test([10, 13, 14, 17], 11, 17, [10, 17]); + test([10, 13, 14, 17], 11, 18, [10, 18]); + test([10, 13, 14, 17], 11, 19, [10, 19]); + test([10, 13, 14, 17], 12, 13, [10, 13, 14, 17]); + test([10, 13, 14, 17], 12, 14, [10, 17]); + test([10, 13, 14, 17], 12, 15, [10, 17]); + test([10, 13, 14, 17], 12, 16, [10, 17]); + test([10, 13, 14, 17], 12, 17, [10, 17]); + test([10, 13, 14, 17], 12, 18, [10, 18]); + test([10, 13, 14, 17], 12, 19, [10, 19]); + test([10, 13, 14, 17], 13, 14, [10, 17]); + test([10, 13, 14, 17], 13, 15, [10, 17]); + test([10, 13, 14, 17], 13, 16, [10, 17]); + test([10, 13, 14, 17], 13, 17, [10, 17]); + test([10, 13, 14, 17], 13, 18, [10, 18]); + test([10, 13, 14, 17], 13, 19, [10, 19]); + test([10, 13, 14, 17], 14, 15, [10, 13, 14, 17]); + test([10, 13, 14, 17], 14, 16, [10, 13, 14, 17]); + test([10, 13, 14, 17], 14, 17, [10, 13, 14, 17]); + test([10, 13, 14, 17], 14, 18, [10, 13, 14, 18]); + test([10, 13, 14, 17], 14, 19, [10, 13, 14, 19]); + test([10, 13, 14, 17], 15, 16, [10, 13, 14, 17]); + test([10, 13, 14, 17], 15, 17, [10, 13, 14, 17]); + test([10, 13, 14, 17], 15, 18, [10, 13, 14, 18]); + test([10, 13, 14, 17], 15, 19, [10, 13, 14, 19]); + test([10, 13, 14, 17], 16, 17, [10, 13, 14, 17]); + test([10, 13, 14, 17], 16, 18, [10, 13, 14, 18]); + test([10, 13, 14, 17], 16, 19, [10, 13, 14, 19]); + test([10, 13, 14, 17], 17, 18, [10, 13, 14, 18]); + test([10, 13, 14, 17], 17, 19, [10, 13, 14, 19]); + test([10, 13, 14, 17], 18, 19, [10, 13, 14, 17, 18, 19]); + + test([10, 13, 16, 19], 7, 14, [ 7, 14, 16, 19]); + test([10, 13, 16, 19], 7, 15, [ 7, 15, 16, 19]); + test([10, 13, 16, 19], 7, 16, [ 7, 19]); + test([10, 13, 16, 19], 8, 14, [ 8, 14, 16, 19]); + test([10, 13, 16, 19], 8, 15, [ 8, 15, 16, 19]); + test([10, 13, 16, 19], 8, 16, [ 8, 19]); + test([10, 13, 16, 19], 9, 14, [ 9, 14, 16, 19]); + test([10, 13, 16, 19], 9, 15, [ 9, 15, 16, 19]); + test([10, 13, 16, 19], 9, 16, [ 9, 19]); + test([10, 13, 16, 19], 10, 14, [10, 14, 16, 19]); + test([10, 13, 16, 19], 10, 15, [10, 15, 16, 19]); + test([10, 13, 16, 19], 10, 16, [10, 19]); + test([10, 13, 16, 19], 11, 14, [10, 14, 16, 19]); + test([10, 13, 16, 19], 11, 15, [10, 15, 16, 19]); + test([10, 13, 16, 19], 11, 16, [10, 19]); + test([10, 13, 16, 19], 12, 14, [10, 14, 16, 19]); + test([10, 13, 16, 19], 12, 15, [10, 15, 16, 19]); + test([10, 13, 16, 19], 12, 16, [10, 19]); + test([10, 13, 16, 19], 13, 14, [10, 14, 16, 19]); + test([10, 13, 16, 19], 13, 15, [10, 15, 16, 19]); + test([10, 13, 16, 19], 13, 16, [10, 19]); + test([10, 13, 16, 19], 14, 15, [10, 13, 14, 15, 16, 19]); + test([10, 13, 16, 19], 14, 16, [10, 13, 14, 19]); + test([10, 13, 16, 19], 15, 16, [10, 13, 15, 19]); + + run_next_test(); +}); + +add_test(function test_ril_consts_cellbroadcast_misc() { + // Must be 16 for indexing. + equal(CB_DCS_LANG_GROUP_1.length, 16); + equal(CB_DCS_LANG_GROUP_2.length, 16); + + // Array length must be even. + equal(CB_NON_MMI_SETTABLE_RANGES.length & 0x01, 0); + for (let i = 0; i < CB_NON_MMI_SETTABLE_RANGES.length;) { + let from = CB_NON_MMI_SETTABLE_RANGES[i++]; + let to = CB_NON_MMI_SETTABLE_RANGES[i++]; + equal(from < to, true); + } + + run_next_test(); +}); + +add_test(function test_ril_worker_checkCellBroadcastMMISettable() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + + function test(from, to, expected) { + equal(expected, ril._checkCellBroadcastMMISettable(from, to)); + } + + test(-2, -1, false); + test(-1, 0, false); + test(0, 1, true); + test(1, 1, false); + test(2, 1, false); + test(65536, 65537, false); + + // We have both [4096, 4224), [4224, 4352), so it's actually [4096, 4352), + // and [61440, 65536), [65535, 65536), so it's actually [61440, 65536). + for (let i = 0; i < CB_NON_MMI_SETTABLE_RANGES.length;) { + let from = CB_NON_MMI_SETTABLE_RANGES[i++]; + let to = CB_NON_MMI_SETTABLE_RANGES[i++]; + if ((from != 4224) && (from != 65535)) { + test(from - 1, from, true); + } + test(from - 1, from + 1, false); + test(from - 1, to, false); + test(from - 1, to + 1, false); + test(from, from + 1, false); + test(from, to, false); + test(from, to + 1, false); + if ((from + 1) < to) { + test(from + 1, to, false); + test(from + 1, to + 1, false); + } + if ((to != 4224) && (to < 65535)) { + test(to, to + 1, true); + test(to + 1, to + 2, true); + } + } + + run_next_test(); +}); + +add_test(function test_ril_worker_CellBroadcastDisabled() { + let count = 0; + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + if (message.rilMessageType == "cellbroadcast-received") { + ok(true, "cellbroadcast-received: " + JSON.stringify(message)); + count++; + } + } + }); + + function buildPdu(aMessageId) { + return "C002" + aMessageId + "011154741914AFA7C76B9058" + + "FEBEBB41E6371EA4AEB7E173D0DB5E96" + + "83E8E832881DD6E741E4F7B9D168341A" + + "8D46A3D168341A8D46A3D168341A8D46" + + "A3D168341A8D46A3D168341A8D46A3D1" + + "68341A8D46A3D100"; + } + + worker.ContextPool._contexts[0].RIL.cellBroadcastDisabled = true; + + let networkAlertIds = [ + "1100", "1107", // ETWS + "1112", "112F", // CMAS + "1130", "18FF", // PWS + ]; + networkAlertIds.forEach(aMessageId => { + worker.onRILMessage( + 0, + newIncomingParcel( + -1, + RESPONSE_TYPE_UNSOLICITED, + UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS, + hexStringToParcelByteArrayData(buildPdu(aMessageId)))); + }); + equal(count, networkAlertIds.length, "Alerts shall not be ignored."); + + count = 0; + let normalMsgIds = [ "0000", "03E7", "1108", "1901" ]; + normalMsgIds.forEach(aMessageId => { + worker.onRILMessage( + 0, + newIncomingParcel( + -1, + RESPONSE_TYPE_UNSOLICITED, + UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS, + hexStringToParcelByteArrayData(buildPdu(aMessageId)))); + }); + equal(count, 0, "Normal messages shall be ignored."); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_cellbroadcast_gsm.js b/dom/system/gonk/tests/test_ril_worker_cellbroadcast_gsm.js new file mode 100644 index 000000000..b08b64135 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_cellbroadcast_gsm.js @@ -0,0 +1,230 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +add_test(function test_ril_worker_GsmPDUHelper_readCbDataCodingScheme() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + function test_dcs(dcs, encoding, language, hasLanguageIndicator, messageClass) { + context.Buf.readUint8 = function() { + return dcs; + }; + + let msg = {}; + context.GsmPDUHelper.readCbDataCodingScheme(msg); + + equal(msg.dcs, dcs); + equal(msg.encoding, encoding); + equal(msg.language, language); + equal(msg.hasLanguageIndicator, hasLanguageIndicator); + equal(msg.messageClass, messageClass); + } + + function test_dcs_throws(dcs) { + context.Buf.readUint8 = function() { + return dcs; + }; + + throws(function() { + context.GsmPDUHelper.readCbDataCodingScheme({}); + }, "Unsupported CBS data coding scheme: " + dcs); + } + + // Group 0000 + for (let i = 0; i < 16; i++) { + test_dcs(i, PDU_DCS_MSG_CODING_7BITS_ALPHABET, CB_DCS_LANG_GROUP_1[i], + false, GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + } + + // Group 0001 + // 0000 GSM 7 bit default alphabet; message preceded by language indication. + test_dcs(0x10, PDU_DCS_MSG_CODING_7BITS_ALPHABET, null, true, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + // 0001 UCS2; message preceded by language indication. + test_dcs(0x11, PDU_DCS_MSG_CODING_16BITS_ALPHABET, null, true, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + + // Group 0010 + // 0000..0100 + for (let i = 0; i < 5; i++) { + test_dcs(0x20 + i, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + CB_DCS_LANG_GROUP_2[i], false, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + } + // 0101..1111 Reserved + for (let i = 5; i < 16; i++) { + test_dcs(0x20 + i, PDU_DCS_MSG_CODING_7BITS_ALPHABET, null, false, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + } + + // Group 0100, 0101, 1001 + for (let group of [0x40, 0x50, 0x90]) { + for (let i = 0; i < 16; i++) { + let encoding = i & 0x0C; + if (encoding == 0x0C) { + encoding = PDU_DCS_MSG_CODING_7BITS_ALPHABET; + } + let messageClass = GECKO_SMS_MESSAGE_CLASSES[i & PDU_DCS_MSG_CLASS_BITS]; + test_dcs(group + i, encoding, null, false, messageClass); + } + } + + // Group 1111 + for (let i = 0; i < 16; i ++) { + let encoding = i & 0x04 ? PDU_DCS_MSG_CODING_8BITS_ALPHABET + : PDU_DCS_MSG_CODING_7BITS_ALPHABET; + let messageClass; + switch(i & PDU_DCS_MSG_CLASS_BITS) { + case 0x01: messageClass = PDU_DCS_MSG_CLASS_USER_1; break; + case 0x02: messageClass = PDU_DCS_MSG_CLASS_USER_2; break; + case 0x03: messageClass = PDU_DCS_MSG_CLASS_3; break; + default: messageClass = PDU_DCS_MSG_CLASS_NORMAL; break; + } + test_dcs(0xF0 + i, encoding, null, false, + GECKO_SMS_MESSAGE_CLASSES[messageClass]); + } + + // Group 0011, 1000, 1010, 1011, 1100 + // 0000..1111 Reserved + for (let group of [0x30, 0x80, 0xA0, 0xB0, 0xC0]) { + for (let i = 0; i < 16; i++) { + test_dcs(group + i, PDU_DCS_MSG_CODING_7BITS_ALPHABET, null, false, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + } + } + + // Group 0110, 0111, 1101, 1110 + // TODO: unsupported + for (let group of [0x60, 0x70, 0xD0, 0xE0]) { + for (let i = 0; i < 16; i++) { + test_dcs_throws(group + i); + } + } + + run_next_test(); +}); + +add_test(function test_ril_worker_GsmPDUHelper_readGsmCbData() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + function test_data(options, expected) { + let readIndex = 0; + context.Buf.readUint8 = function() { + return options[3][readIndex++]; + }; + context.Buf.readUint8Array = function(length) { + let array = new Uint8Array(length); + for (let i = 0; i < length; i++) { + array[i] = this.readUint8(); + } + return array; + }; + + let msg = { + encoding: options[0], + language: options[1], + hasLanguageIndicator: options[2] + }; + context.GsmPDUHelper.readGsmCbData(msg, options[3].length); + + equal(msg.body, expected[0]); + equal(msg.data == null, expected[1] == null); + if (expected[1] != null) { + equal(msg.data.length, expected[1].length); + for (let i = 0; i < expected[1].length; i++) { + equal(msg.data[i], expected[1][i]); + } + } + equal(msg.language, expected[2]); + } + + // We're testing Cell Broadcast message body with all zeros octet stream. As + // shown in 3GPP TS 23.038, septet 0x00 will be decoded as '@' when both + // langTableIndex and langShiftTableIndex equal to + // PDU_DCS_MSG_CODING_7BITS_ALPHABET. + + // PDU_DCS_MSG_CODING_7BITS_ALPHABET + test_data([PDU_DCS_MSG_CODING_7BITS_ALPHABET, null, false, + [0]], + ["@", null, null]); + test_data([PDU_DCS_MSG_CODING_7BITS_ALPHABET, null, true, + [0, 0, 0, 0]], + ["@", null, "@@"]); + test_data([PDU_DCS_MSG_CODING_7BITS_ALPHABET, "@@", false, + [0]], + ["@", null, "@@"]); + + // PDU_DCS_MSG_CODING_8BITS_ALPHABET + test_data([PDU_DCS_MSG_CODING_8BITS_ALPHABET, null, false, + [0]], + [null, [0], null]); + + // PDU_DCS_MSG_CODING_16BITS_ALPHABET + test_data([PDU_DCS_MSG_CODING_16BITS_ALPHABET, null, false, + [0x00, 0x40]], + ["@", null, null]); + test_data([PDU_DCS_MSG_CODING_16BITS_ALPHABET, null, true, + [0x00, 0x00, 0x00, 0x40]], + ["@", null, "@@"]); + test_data([PDU_DCS_MSG_CODING_16BITS_ALPHABET, "@@", false, + [0x00, 0x40]], + ["@", null, "@@"]); + + run_next_test(); +}); + +add_test(function test_ril_worker_Sim_Download_Message() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + ok(message.rilMessageType !== "cellbroadcast-received", + "Data-Download message shall be ignored."); + } + }); + + function buildPdu(aMessageId) { + return "C002" + aMessageId + "011154741914AFA7C76B9058" + + "FEBEBB41E6371EA4AEB7E173D0DB5E96" + + "83E8E832881DD6E741E4F7B9D168341A" + + "8D46A3D168341A8D46A3D168341A8D46" + + "A3D168341A8D46A3D168341A8D46A3D1" + + "68341A8D46A3D100"; + } + + ["1000", "107F", "1080", "10FF"].forEach(aMessageId => { + worker.onRILMessage( + 0, + newIncomingParcel( + -1, + RESPONSE_TYPE_UNSOLICITED, + UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS, + hexStringToParcelByteArrayData(buildPdu(aMessageId)))); + }); + + ok(true, "All Data-Download Messages are ingored."); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_cellbroadcast_umts.js b/dom/system/gonk/tests/test_ril_worker_cellbroadcast_umts.js new file mode 100644 index 000000000..0380c4122 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_cellbroadcast_umts.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +function buildHexStr(aNum, aNumSemiOctets) { + let str = aNum.toString(16); + while (str.length < aNumSemiOctets) { + str = "0" + str; + } + return str; +} + +/** + * Verify GsmPDUHelper#readUmtsCbMessage with numOfPages from 1 to 15. + */ +add_test(function test_GsmPDUHelper_readUmtsCbMessage_MultiParts() { + let CB_UMTS_MESSAGE_PAGE_SIZE = 82; + let CB_MAX_CONTENT_PER_PAGE_7BIT = 93; + let workerHelper = newInterceptWorker(), + worker = workerHelper.worker, + context = worker.ContextPool._contexts[0], + GsmPDUHelper = context.GsmPDUHelper; + + function test_MultiParts(aNumOfPages) { + let pdu = buildHexStr(CB_UMTS_MESSAGE_TYPE_CBS, 2) // msg_type + + buildHexStr(0, 4) // skip msg_id + + buildHexStr(0, 4) // skip SN + + buildHexStr(0, 2) // skip dcs + + buildHexStr(aNumOfPages, 2); // set num_of_pages + for (let i = 1; i <= aNumOfPages; i++) { + pdu = pdu + buildHexStr(0, CB_UMTS_MESSAGE_PAGE_SIZE * 2) + + buildHexStr(CB_UMTS_MESSAGE_PAGE_SIZE, 2); // msg_info_length + } + + worker.onRILMessage(0, newIncomingParcel(-1, + RESPONSE_TYPE_UNSOLICITED, + UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS, + hexStringToParcelByteArrayData(pdu))); + + let postedMessage = workerHelper.postedMessage; + equal("cellbroadcast-received", postedMessage.rilMessageType); + equal(postedMessage.fullBody.length, + aNumOfPages * CB_MAX_CONTENT_PER_PAGE_7BIT); + } + + [1, 5, 15].forEach(function(i) { + test_MultiParts(i); + }); + + run_next_test(); +}); + +/** + * Verify GsmPDUHelper#readUmtsCbMessage with 8bit encoded. + */ +add_test(function test_GsmPDUHelper_readUmtsCbMessage_Binary() { + let CB_UMTS_MESSAGE_PAGE_SIZE = 82; + let CB_MAX_CONTENT_PER_PAGE_7BIT = 93; + let TEXT_BINARY = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFF"; + let workerHelper = newInterceptWorker(), + worker = workerHelper.worker, + context = worker.ContextPool._contexts[0], + GsmPDUHelper = context.GsmPDUHelper; + + function test_MultiPartsBinary(aNumOfPages) { + let pdu = buildHexStr(CB_UMTS_MESSAGE_TYPE_CBS, 2) // msg_type + + buildHexStr(0, 4) // skip msg_id + + buildHexStr(0, 4) // skip SN + + buildHexStr(68, 2) // set DCS to 8bit data + + buildHexStr(aNumOfPages, 2); // set num_of_pages + for (let i = 1; i <= aNumOfPages; i++) { + pdu = pdu + TEXT_BINARY + + buildHexStr(CB_UMTS_MESSAGE_PAGE_SIZE, 2); // msg_info_length + } + + worker.onRILMessage(0, newIncomingParcel(-1, + RESPONSE_TYPE_UNSOLICITED, + UNSOLICITED_RESPONSE_NEW_BROADCAST_SMS, + hexStringToParcelByteArrayData(pdu))); + + let postedMessage = workerHelper.postedMessage; + equal("cellbroadcast-received", postedMessage.rilMessageType); + equal(postedMessage.fullData.length, + aNumOfPages * CB_UMTS_MESSAGE_PAGE_SIZE); + for (let i = 0; i < postedMessage.fullData.length; i++) { + equal(postedMessage.fullData[i], 255); + } + } + + [1, 5, 15].forEach(function(i) { + test_MultiPartsBinary(i); + }); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_cf.js b/dom/system/gonk/tests/test_ril_worker_cf.js new file mode 100644 index 000000000..b8db716b7 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_cf.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +function toaFromString(number) { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + let context = worker.ContextPool._contexts[0]; + return context.RIL._toaFromString(number); +} + +add_test(function test_toaFromString_empty() { + let retval = toaFromString(""); + + equal(retval, TOA_UNKNOWN); + + run_next_test(); +}); + +add_test(function test_toaFromString_undefined() { + let retval = toaFromString(); + + equal(retval, TOA_UNKNOWN); + + run_next_test(); +}); + +add_test(function test_toaFromString_unknown() { + let retval = toaFromString("666222333"); + + equal(retval, TOA_UNKNOWN); + + run_next_test(); +}); + +add_test(function test_toaFromString_international() { + let retval = toaFromString("+34666222333"); + + equal(retval, TOA_INTERNATIONAL); + + run_next_test(); +}); + +add_test(function test_setCallForward_unconditional() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL.setCallForward = function fakeSetCallForward(options) { + context.RIL[REQUEST_SET_CALL_FORWARD](0, {}); + }; + + context.RIL.setCallForward({ + action: CALL_FORWARD_ACTION_REGISTRATION, + reason: CALL_FORWARD_REASON_UNCONDITIONAL, + serviceClass: ICC_SERVICE_CLASS_VOICE, + number: "666222333", + timeSeconds: 10 + }); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + + run_next_test(); +}); + +add_test(function test_queryCallForwardStatus_unconditional() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL.setCallForward = function fakeSetCallForward(options) { + context.RIL[REQUEST_SET_CALL_FORWARD](0, {}); + }; + + context.Buf.readInt32 = function fakeReadUint32() { + return context.Buf.int32Array.pop(); + }; + + context.Buf.readString = function fakeReadString() { + return "+34666222333"; + }; + + context.RIL.queryCallForwardStatus = function fakeQueryCallForward(options) { + context.Buf.int32Array = [ + 0, // rules.timeSeconds + 145, // rules.toa + 49, // rules.serviceClass + CALL_FORWARD_REASON_UNCONDITIONAL, // rules.reason + 1, // rules.active + 1 // rulesLength + ]; + context.RIL[REQUEST_QUERY_CALL_FORWARD_STATUS](1, {}); + }; + + context.RIL.queryCallForwardStatus({ + action: CALL_FORWARD_ACTION_QUERY_STATUS, + reason: CALL_FORWARD_REASON_UNCONDITIONAL, + serviceClass: ICC_SERVICE_CLASS_VOICE, + number: "666222333", + timeSeconds: 10 + }); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + ok(Array.isArray(postedMessage.rules)); + do_print(postedMessage.rules.length); + equal(postedMessage.rules.length, 1); + ok(postedMessage.rules[0].active); + equal(postedMessage.rules[0].reason, CALL_FORWARD_REASON_UNCONDITIONAL); + equal(postedMessage.rules[0].number, "+34666222333"); + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_clip.js b/dom/system/gonk/tests/test_ril_worker_clip.js new file mode 100644 index 000000000..d1ce5f617 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_clip.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +add_test(function test_queryCLIP_provisioned() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.Buf.readInt32 = function fakeReadUint32() { + return context.Buf.int32Array.pop(); + }; + + context.RIL.queryCLIP = function fakeQueryCLIP(options) { + context.Buf.int32Array = [ + 1, // CLIP provisioned. + 1 // Length. + ]; + context.RIL[REQUEST_QUERY_CLIP](1, {}); + }; + + context.RIL.queryCLIP({}); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + equal(postedMessage.provisioned, 1); + run_next_test(); +}); + +add_test(function test_getCLIP_error_generic_failure_invalid_length() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.Buf.readInt32 = function fakeReadUint32() { + return context.Buf.int32Array.pop(); + }; + + context.RIL.queryCLIP = function fakeQueryCLIP(options) { + context.Buf.int32Array = [ + 1, // CLIP provisioned. + 0 // Length. + ]; + context.RIL[REQUEST_QUERY_CLIP](1, {}); + }; + + context.RIL.queryCLIP({}); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, GECKO_ERROR_GENERIC_FAILURE); + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_clir.js b/dom/system/gonk/tests/test_ril_worker_clir.js new file mode 100644 index 000000000..5882a3c4c --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_clir.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +// Calling line identification restriction constants. + +// Uses subscription default value. +const CLIR_DEFAULT = 0; +// Restricts CLI presentation. +const CLIR_INVOCATION = 1; +// Allows CLI presentation. +const CLIR_SUPPRESSION = 2; + +function run_test() { + run_next_test(); +} + +add_test(function test_setCLIR_success() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL.setCLIR = function fakeSetCLIR(options) { + context.RIL[REQUEST_SET_CLIR](0, { + rilMessageType: "setCLIR" + }); + }; + + context.RIL.setCLIR({ + clirMode: CLIR_DEFAULT + }); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + + run_next_test(); +}); + +add_test(function test_setCLIR_generic_failure() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL.setCLIR = function fakeSetCLIR(options) { + context.RIL[REQUEST_SET_CLIR](0, { + rilMessageType: "setCLIR", + errorMsg: GECKO_ERROR_GENERIC_FAILURE + }); + }; + + context.RIL.setCLIR({ + clirMode: CLIR_DEFAULT + }); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, GECKO_ERROR_GENERIC_FAILURE); + + run_next_test(); +}); + +add_test(function test_getCLIR_n0_m1() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.Buf.readInt32 = function fakeReadUint32() { + return context.Buf.int32Array.pop(); + }; + + context.RIL.getCLIR = function fakeGetCLIR(options) { + context.Buf.int32Array = [ + 1, // Presentation indicator is used according to the subscription + // of the CLIR service. + 0, // CLIR provisioned in permanent mode. + 2 // Length. + ]; + context.RIL[REQUEST_GET_CLIR](1, { + rilMessageType: "setCLIR" + }); + }; + + context.RIL.getCLIR({}); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + equal(postedMessage.n, 0); + equal(postedMessage.m, 1); + run_next_test(); +}); + +add_test(function test_getCLIR_error_generic_failure_invalid_length() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.Buf.readInt32 = function fakeReadUint32() { + return context.Buf.int32Array.pop(); + }; + + context.RIL.getCLIR = function fakeGetCLIR(options) { + context.Buf.int32Array = [ + 1, // Presentation indicator is used according to the subscription + // of the CLIR service. + 0, // CLIR provisioned in permanent mode. + 0 // Length (invalid one). + ]; + context.RIL[REQUEST_GET_CLIR](1, { + rilMessageType: "setCLIR" + }); + }; + + context.RIL.getCLIR({}); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, GECKO_ERROR_GENERIC_FAILURE); + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_cw.js b/dom/system/gonk/tests/test_ril_worker_cw.js new file mode 100644 index 000000000..efa8b5c21 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_cw.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +add_test(function test_setCallWaiting_success() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL.setCallWaiting = function fakeSetCallWaiting(options) { + context.RIL[REQUEST_SET_CALL_WAITING](0, {}); + }; + + context.RIL.setCallWaiting({ + enabled: true + }); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + + run_next_test(); +}); + +add_test(function test_setCallWaiting_generic_failure() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL.setCallWaiting = function fakeSetCallWaiting(options) { + context.RIL[REQUEST_SET_CALL_WAITING](0, { + errorMsg: GECKO_ERROR_GENERIC_FAILURE + }); + }; + + context.RIL.setCallWaiting({ + enabled: true + }); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, GECKO_ERROR_GENERIC_FAILURE); + + run_next_test(); +}); + +add_test(function test_queryCallWaiting_success_enabled_true() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.Buf.readInt32 = function fakeReadUint32() { + return context.Buf.int32Array.pop(); + }; + + context.RIL.queryCallWaiting = function fakeQueryCallWaiting(options) { + context.Buf.int32Array = [ + 1, // serviceClass + 1, // enabled + 2 // length + ]; + context.RIL[REQUEST_QUERY_CALL_WAITING](1, {}); + }; + + context.RIL.queryCallWaiting({}); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + equal(postedMessage.serviceClass, 1); + run_next_test(); +}); + +add_test(function test_queryCallWaiting_success_enabled_false() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.Buf.readInt32 = function fakeReadUint32() { + return context.Buf.int32Array.pop(); + }; + + context.RIL.queryCallWaiting = function fakeQueryCallWaiting(options) { + context.Buf.int32Array = [ + 1, // serviceClass + 0, // enabled + 2 // length + ]; + context.RIL[REQUEST_QUERY_CALL_WAITING](1, {}); + }; + + context.RIL.queryCallWaiting({}); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + equal(postedMessage.serviceClass, ICC_SERVICE_CLASS_NONE); + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_ecm.js b/dom/system/gonk/tests/test_ril_worker_ecm.js new file mode 100644 index 000000000..d10cba9ec --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_ecm.js @@ -0,0 +1,168 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +var timeoutCallback = null; +var timeoutDelayMs = 0; +const TIMER_ID = 1234; +const TIMEOUT_VALUE = 300000; // 5 mins. + +// No window in xpcshell-test. Create our own timer mechanism. + +function setTimeout(callback, timeoutMs) { + timeoutCallback = callback; + timeoutDelayMs = timeoutMs; + equal(timeoutMs, TIMEOUT_VALUE); + return TIMER_ID; +} + +function clearTimeout(timeoutId) { + equal(timeoutId, TIMER_ID); + timeoutCallback = null; +} + +function fireTimeout() { + notEqual(timeoutCallback, null); + if (timeoutCallback) { + timeoutCallback(); + timeoutCallback = null; + } +} + +add_test(function test_enter_emergencyCbMode() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + // Do it twice. Should always send the event. + for (let i = 0; i < 2; ++i) { + context.RIL[UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE](); + let postedMessage = workerHelper.postedMessage; + + // Should store the mode. + equal(context.RIL._isInEmergencyCbMode, true); + + // Should notify change. + equal(postedMessage.rilMessageType, "emergencyCbModeChange"); + equal(postedMessage.active, true); + equal(postedMessage.timeoutMs, TIMEOUT_VALUE); + + // Should start timer. + equal(context.RIL._exitEmergencyCbModeTimeoutID, TIMER_ID); + } + + run_next_test(); +}); + +add_test(function test_exit_emergencyCbMode() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL[UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE](); + context.RIL[UNSOLICITED_EXIT_EMERGENCY_CALLBACK_MODE](); + let postedMessage = workerHelper.postedMessage; + + // Should store the mode. + equal(context.RIL._isInEmergencyCbMode, false); + + // Should notify change. + equal(postedMessage.rilMessageType, "emergencyCbModeChange"); + equal(postedMessage.active, false); + + // Should clear timer. + equal(context.RIL._exitEmergencyCbModeTimeoutID, null); + + run_next_test(); +}); + +add_test(function test_request_exit_emergencyCbMode_when_timeout() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL[UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE](); + equal(context.RIL._isInEmergencyCbMode, true); + equal(context.RIL._exitEmergencyCbModeTimeoutID, TIMER_ID); + + let parcelTypes = []; + context.Buf.newParcel = function(type, options) { + parcelTypes.push(type); + }; + + // Timeout. + fireTimeout(); + + // Should clear timeout event. + equal(context.RIL._exitEmergencyCbModeTimeoutID, null); + + // Check indeed sent out REQUEST_EXIT_EMERGENCY_CALLBACK_MODE. + notEqual(parcelTypes.indexOf(REQUEST_EXIT_EMERGENCY_CALLBACK_MODE), -1); + + run_next_test(); +}); + +add_test(function test_request_exit_emergencyCbMode_when_dial() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL[UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE](); + equal(context.RIL._isInEmergencyCbMode, true); + equal(context.RIL._exitEmergencyCbModeTimeoutID, TIMER_ID); + + let parcelTypes = []; + context.Buf.newParcel = function(type, options) { + parcelTypes.push(type); + }; + + // Dial non-emergency call. + context.RIL.dial({number: "0912345678", + isEmergency: false, + isDialEmergency: false}); + + // Should clear timeout event. + equal(context.RIL._exitEmergencyCbModeTimeoutID, null); + + // Check indeed sent out REQUEST_EXIT_EMERGENCY_CALLBACK_MODE. + notEqual(parcelTypes.indexOf(REQUEST_EXIT_EMERGENCY_CALLBACK_MODE), -1); + + run_next_test(); +}); + +add_test(function test_request_exit_emergencyCbMode_explicitly() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL[UNSOLICITED_ENTER_EMERGENCY_CALLBACK_MODE](); + equal(context.RIL._isInEmergencyCbMode, true); + equal(context.RIL._exitEmergencyCbModeTimeoutID, TIMER_ID); + + let parcelTypes = []; + context.Buf.newParcel = function(type, options) { + parcelTypes.push(type); + }; + + context.RIL.handleChromeMessage({rilMessageType: "exitEmergencyCbMode"}); + context.RIL[REQUEST_EXIT_EMERGENCY_CALLBACK_MODE](1, { + rilMessageType: "exitEmergencyCbMode" + }); + let postedMessage = workerHelper.postedMessage; + + // Should clear timeout event. + equal(context.RIL._exitEmergencyCbModeTimeoutID, null); + + // Check indeed sent out REQUEST_EXIT_EMERGENCY_CALLBACK_MODE. + notEqual(parcelTypes.indexOf(REQUEST_EXIT_EMERGENCY_CALLBACK_MODE), -1); + + // Send back the response. + equal(postedMessage.rilMessageType, "exitEmergencyCbMode"); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_icc_BerTlvHelper.js b/dom/system/gonk/tests/test_ril_worker_icc_BerTlvHelper.js new file mode 100644 index 000000000..89fcd874d --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_BerTlvHelper.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +// Test ICC_COMMAND_GET_RESPONSE with FCP template format. +/** + * Verify transparent structure with FCP template format. + */ +add_test(function test_fcp_template_for_transparent_structure() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let berHelper = context.BerTlvHelper; + + let tag_test = [ + 0x62, + 0x22, + 0x82, 0x02, 0x41, 0x21, + 0x83, 0x02, 0x2F, 0xE2, + 0xA5, 0x09, 0xC1, 0x04, 0x40, 0x0F, 0xF5, 0x55, 0x92, 0x01, 0x00, + 0x8A, 0x01, 0x05, + 0x8B, 0x03, 0x2F, 0x06, 0x0B, + 0x80, 0x02, 0x00, 0x0A, + 0x88, 0x01, 0x10]; + + for (let i = 0; i < tag_test.length; i++) { + pduHelper.writeHexOctet(tag_test[i]); + } + + let berTlv = berHelper.decode(tag_test.length); + let iter = berTlv.value.values(); + let tlv = berHelper.searchForNextTag(BER_FCP_FILE_DESCRIPTOR_TAG, iter); + equal(tlv.value.fileStructure, UICC_EF_STRUCTURE[EF_STRUCTURE_TRANSPARENT]); + + tlv = berHelper.searchForNextTag(BER_FCP_FILE_IDENTIFIER_TAG, iter); + equal(tlv.value.fileId, 0x2FE2); + + tlv = berHelper.searchForNextTag(BER_FCP_FILE_SIZE_DATA_TAG, iter); + equal(tlv.value.fileSizeData, 0x0A); + + run_next_test(); +}); + +/** + * Verify linear fixed structure with FCP template format. + */ +add_test(function test_fcp_template_for_linear_fixed_structure() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let berHelper = context.BerTlvHelper; + + let tag_test = [ + 0x62, + 0x1E, + 0x82, 0x05, 0x42, 0x21, 0x00, 0x1A, 0x01, + 0x83, 0x02, 0x6F, 0x40, + 0xA5, 0x03, 0x92, 0x01, 0x00, + 0x8A, 0x01, 0x07, + 0x8B, 0x03, 0x6F, 0x06, 0x02, + 0x80, 0x02, 0x00, 0x1A, + 0x88, 0x00]; + + for (let i = 0; i < tag_test.length; i++) { + pduHelper.writeHexOctet(tag_test[i]); + } + + let berTlv = berHelper.decode(tag_test.length); + let iter = berTlv.value.values(); + let tlv = berHelper.searchForNextTag(BER_FCP_FILE_DESCRIPTOR_TAG, iter); + equal(tlv.value.fileStructure, UICC_EF_STRUCTURE[EF_STRUCTURE_LINEAR_FIXED]); + equal(tlv.value.recordLength, 0x1A); + equal(tlv.value.numOfRecords, 0x01); + + tlv = berHelper.searchForNextTag(BER_FCP_FILE_IDENTIFIER_TAG, iter); + equal(tlv.value.fileId, 0x6F40); + + tlv = berHelper.searchForNextTag(BER_FCP_FILE_SIZE_DATA_TAG, iter); + equal(tlv.value.fileSizeData, 0x1A); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_icc_CardLock.js b/dom/system/gonk/tests/test_ril_worker_icc_CardLock.js new file mode 100644 index 000000000..dc7eb93b9 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_CardLock.js @@ -0,0 +1,282 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify RIL.iccGetCardLockEnabled + */ +add_test(function test_icc_get_card_lock_enabled() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let ril = context.RIL; + ril.aid = "123456789"; + + function do_test(aLock) { + const serviceClass = ICC_SERVICE_CLASS_VOICE | + ICC_SERVICE_CLASS_DATA | + ICC_SERVICE_CLASS_FAX; + + buf.sendParcel = function fakeSendParcel() { + // Request Type. + equal(this.readInt32(), REQUEST_QUERY_FACILITY_LOCK) + + // Token : we don't care. + this.readInt32(); + + // Data + let parcel = this.readStringList(); + equal(parcel.length, 4); + equal(parcel[0], GECKO_CARDLOCK_TO_FACILITY[aLock]); + equal(parcel[1], ""); + equal(parcel[2], serviceClass.toString()); + equal(parcel[3], ril.aid); + }; + + ril.iccGetCardLockEnabled({lockType: aLock}); + } + + do_test(GECKO_CARDLOCK_PIN) + do_test(GECKO_CARDLOCK_FDN) + + run_next_test(); +}); + +add_test(function test_path_id_for_spid_and_spn() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + }}); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let ICCFileHelper = context.ICCFileHelper; + + // Test SIM + RIL.appType = CARD_APPTYPE_SIM; + equal(ICCFileHelper.getEFPath(ICC_EF_SPDI), + EF_PATH_MF_SIM + EF_PATH_DF_GSM); + equal(ICCFileHelper.getEFPath(ICC_EF_SPN), + EF_PATH_MF_SIM + EF_PATH_DF_GSM); + + // Test USIM + RIL.appType = CARD_APPTYPE_USIM; + equal(ICCFileHelper.getEFPath(ICC_EF_SPDI), + EF_PATH_MF_SIM + EF_PATH_ADF_USIM); + equal(ICCFileHelper.getEFPath(ICC_EF_SPDI), + EF_PATH_MF_SIM + EF_PATH_ADF_USIM); + run_next_test(); +}); + +/** + * Verify RIL.iccSetCardLockEnabled + */ +add_test(function test_icc_set_card_lock_enabled() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let ril = context.RIL; + ril.aid = "123456789"; + + function do_test(aLock, aPassword, aEnabled) { + const serviceClass = ICC_SERVICE_CLASS_VOICE | + ICC_SERVICE_CLASS_DATA | + ICC_SERVICE_CLASS_FAX; + + buf.sendParcel = function fakeSendParcel() { + // Request Type. + equal(this.readInt32(), REQUEST_SET_FACILITY_LOCK); + + // Token : we don't care + this.readInt32(); + + // Data + let parcel = this.readStringList(); + equal(parcel.length, 5); + equal(parcel[0], GECKO_CARDLOCK_TO_FACILITY[aLock]); + equal(parcel[1], aEnabled ? "1" : "0"); + equal(parcel[2], aPassword); + equal(parcel[3], serviceClass.toString()); + equal(parcel[4], ril.aid); + }; + + ril.iccSetCardLockEnabled({ + lockType: aLock, + enabled: aEnabled, + password: aPassword}); + } + + do_test(GECKO_CARDLOCK_PIN, "1234", true); + do_test(GECKO_CARDLOCK_PIN, "1234", false); + do_test(GECKO_CARDLOCK_FDN, "4321", true); + do_test(GECKO_CARDLOCK_FDN, "4321", false); + + run_next_test(); +}); + +/** + * Verify RIL.iccChangeCardLockPassword + */ +add_test(function test_icc_change_card_lock_password() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let ril = context.RIL; + + + function do_test(aLock, aPassword, aNewPassword) { + let GECKO_CARDLOCK_TO_REQUEST = {}; + GECKO_CARDLOCK_TO_REQUEST[GECKO_CARDLOCK_PIN] = REQUEST_CHANGE_SIM_PIN; + GECKO_CARDLOCK_TO_REQUEST[GECKO_CARDLOCK_PIN2] = REQUEST_CHANGE_SIM_PIN2; + + buf.sendParcel = function fakeSendParcel() { + // Request Type. + equal(this.readInt32(), GECKO_CARDLOCK_TO_REQUEST[aLock]); + + // Token : we don't care + this.readInt32(); + + // Data + let parcel = this.readStringList(); + equal(parcel.length, 3); + equal(parcel[0], aPassword); + equal(parcel[1], aNewPassword); + equal(parcel[2], ril.aid); + }; + + ril.iccChangeCardLockPassword({ + lockType: aLock, + password: aPassword, + newPassword: aNewPassword}); + } + + do_test(GECKO_CARDLOCK_PIN, "1234", "4321"); + do_test(GECKO_CARDLOCK_PIN2, "1234", "4321"); + + run_next_test(); +}); + +/** + * Verify RIL.iccUnlockCardLock - PIN + */ +add_test(function test_icc_unlock_card_lock_pin() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let buf = context.Buf; + ril.aid = "123456789"; + + function do_test(aLock, aPassword) { + let GECKO_CARDLOCK_TO_REQUEST = {}; + GECKO_CARDLOCK_TO_REQUEST[GECKO_CARDLOCK_PIN] = REQUEST_ENTER_SIM_PIN; + GECKO_CARDLOCK_TO_REQUEST[GECKO_CARDLOCK_PIN2] = REQUEST_ENTER_SIM_PIN2; + + buf.sendParcel = function fakeSendParcel() { + // Request Type. + equal(this.readInt32(), GECKO_CARDLOCK_TO_REQUEST[aLock]); + + // Token : we don't care + this.readInt32(); + + // Data + let parcel = this.readStringList(); + equal(parcel.length, 2); + equal(parcel[0], aPassword); + equal(parcel[1], ril.aid); + }; + + ril.iccUnlockCardLock({ + lockType: aLock, + password: aPassword + }); + } + + do_test(GECKO_CARDLOCK_PIN, "1234"); + do_test(GECKO_CARDLOCK_PIN2, "1234"); + + run_next_test(); +}); + +/** + * Verify RIL.iccUnlockCardLock - PUK + */ +add_test(function test_icc_unlock_card_lock_puk() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let buf = context.Buf; + ril.aid = "123456789"; + + function do_test(aLock, aPassword, aNewPin) { + let GECKO_CARDLOCK_TO_REQUEST = {}; + GECKO_CARDLOCK_TO_REQUEST[GECKO_CARDLOCK_PUK] = REQUEST_ENTER_SIM_PUK; + GECKO_CARDLOCK_TO_REQUEST[GECKO_CARDLOCK_PUK2] = REQUEST_ENTER_SIM_PUK2; + + buf.sendParcel = function fakeSendParcel() { + // Request Type. + equal(this.readInt32(), GECKO_CARDLOCK_TO_REQUEST[aLock]); + + // Token : we don't care + this.readInt32(); + + // Data + let parcel = this.readStringList(); + equal(parcel.length, 3); + equal(parcel[0], aPassword); + equal(parcel[1], aNewPin); + equal(parcel[2], ril.aid); + }; + + ril.iccUnlockCardLock({ + lockType: aLock, + password: aPassword, + newPin: aNewPin + }); + } + + do_test(GECKO_CARDLOCK_PUK, "12345678", "1234"); + do_test(GECKO_CARDLOCK_PUK2, "12345678", "1234"); + + run_next_test(); +}); + +/** + * Verify RIL.iccUnlockCardLock - Depersonalization + */ +add_test(function test_icc_unlock_card_lock_depersonalization() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let buf = context.Buf; + + function do_test(aPassword) { + buf.sendParcel = function fakeSendParcel() { + // Request Type. + equal(this.readInt32(), REQUEST_ENTER_NETWORK_DEPERSONALIZATION_CODE); + + // Token : we don't care + this.readInt32(); + + // Data + let parcel = this.readStringList(); + equal(parcel.length, 1); + equal(parcel[0], aPassword); + }; + + ril.iccUnlockCardLock({ + lockType: GECKO_CARDLOCK_NCK, + password: aPassword + }); + } + + do_test("12345678"); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_icc_CardState.js b/dom/system/gonk/tests/test_ril_worker_icc_CardState.js new file mode 100644 index 000000000..788df5073 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_CardState.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +add_test(function test_personalization_state() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + + context.ICCRecordHelper.readICCID = function fakeReadICCID() {}; + + function testPersonalization(isCdma, cardPersoState, geckoCardState) { + let iccStatus = { + cardState: CARD_STATE_PRESENT, + gsmUmtsSubscriptionAppIndex: (!isCdma) ? 0 : -1, + cdmaSubscriptionAppIndex: (isCdma) ? 0 : -1, + apps: [ + { + app_state: CARD_APPSTATE_SUBSCRIPTION_PERSO, + perso_substate: cardPersoState + }], + }; + + ril._isCdma = isCdma; + ril._processICCStatus(iccStatus); + equal(ril.cardState, geckoCardState); + } + + // Test GSM personalization state. + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_NETWORK, + Ci.nsIIcc.CARD_STATE_NETWORK_LOCKED); + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_NETWORK_SUBSET, + Ci.nsIIcc.CARD_STATE_NETWORK_SUBSET_LOCKED); + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_CORPORATE, + Ci.nsIIcc.CARD_STATE_CORPORATE_LOCKED); + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_SERVICE_PROVIDER, + Ci.nsIIcc.CARD_STATE_SERVICE_PROVIDER_LOCKED); + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_SIM, + Ci.nsIIcc.CARD_STATE_SIM_LOCKED); + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_NETWORK_PUK, + Ci.nsIIcc.CARD_STATE_NETWORK_PUK_REQUIRED); + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK, + Ci.nsIIcc.CARD_STATE_NETWORK_SUBSET_PUK_REQUIRED); + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_CORPORATE_PUK, + Ci.nsIIcc.CARD_STATE_CORPORATE_PUK_REQUIRED); + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK, + Ci.nsIIcc.CARD_STATE_SERVICE_PROVIDER_PUK_REQUIRED); + testPersonalization(false, CARD_PERSOSUBSTATE_SIM_SIM_PUK, + Ci.nsIIcc.CARD_STATE_SIM_PUK_REQUIRED); + + testPersonalization(false, CARD_PERSOSUBSTATE_UNKNOWN, + Ci.nsIIcc.CARD_STATE_UNKNOWN); + testPersonalization(false, CARD_PERSOSUBSTATE_IN_PROGRESS, + Ci.nsIIcc.CARD_STATE_PERSONALIZATION_IN_PROGRESS); + testPersonalization(false, CARD_PERSOSUBSTATE_READY, + Ci.nsIIcc.CARD_STATE_PERSONALIZATION_READY); + + // Test CDMA personalization state. + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_NETWORK1, + Ci.nsIIcc.CARD_STATE_NETWORK1_LOCKED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_NETWORK2, + Ci.nsIIcc.CARD_STATE_NETWORK2_LOCKED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_HRPD, + Ci.nsIIcc.CARD_STATE_HRPD_NETWORK_LOCKED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_CORPORATE, + Ci.nsIIcc.CARD_STATE_RUIM_CORPORATE_LOCKED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_SERVICE_PROVIDER, + Ci.nsIIcc.CARD_STATE_RUIM_SERVICE_PROVIDER_LOCKED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_RUIM, + Ci.nsIIcc.CARD_STATE_RUIM_LOCKED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_NETWORK1_PUK, + Ci.nsIIcc.CARD_STATE_NETWORK1_PUK_REQUIRED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_NETWORK2_PUK, + Ci.nsIIcc.CARD_STATE_NETWORK2_PUK_REQUIRED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_HRPD_PUK, + Ci.nsIIcc.CARD_STATE_HRPD_NETWORK_PUK_REQUIRED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_CORPORATE_PUK, + Ci.nsIIcc.CARD_STATE_RUIM_CORPORATE_PUK_REQUIRED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK, + Ci.nsIIcc.CARD_STATE_RUIM_SERVICE_PROVIDER_PUK_REQUIRED); + testPersonalization(true, CARD_PERSOSUBSTATE_RUIM_RUIM_PUK, + Ci.nsIIcc.CARD_STATE_RUIM_PUK_REQUIRED); + + testPersonalization(true, CARD_PERSOSUBSTATE_UNKNOWN, + Ci.nsIIcc.CARD_STATE_UNKNOWN); + testPersonalization(true, CARD_PERSOSUBSTATE_IN_PROGRESS, + Ci.nsIIcc.CARD_STATE_PERSONALIZATION_IN_PROGRESS); + testPersonalization(true, CARD_PERSOSUBSTATE_READY, + Ci.nsIIcc.CARD_STATE_PERSONALIZATION_READY); + + run_next_test(); +}); + +/** + * Verify SIM app_state in _processICCStatus + */ +add_test(function test_card_app_state() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + + context.ICCRecordHelper.readICCID = function fakeReadICCID() {}; + + function testCardAppState(cardAppState, geckoCardState) { + let iccStatus = { + cardState: CARD_STATE_PRESENT, + gsmUmtsSubscriptionAppIndex: 0, + apps: [ + { + app_state: cardAppState + }], + }; + + ril._processICCStatus(iccStatus); + equal(ril.cardState, geckoCardState); + } + + testCardAppState(CARD_APPSTATE_ILLEGAL, + Ci.nsIIcc.CARD_STATE_ILLEGAL); + testCardAppState(CARD_APPSTATE_PIN, + Ci.nsIIcc.CARD_STATE_PIN_REQUIRED); + testCardAppState(CARD_APPSTATE_PUK, + Ci.nsIIcc.CARD_STATE_PUK_REQUIRED); + testCardAppState(CARD_APPSTATE_READY, + Ci.nsIIcc.CARD_STATE_READY); + testCardAppState(CARD_APPSTATE_UNKNOWN, + Ci.nsIIcc.CARD_STATE_UNKNOWN); + testCardAppState(CARD_APPSTATE_DETECTED, + Ci.nsIIcc.CARD_STATE_UNKNOWN); + + run_next_test(); +}); + +/** + * Verify permanent blocked for ICC. + */ +add_test(function test_icc_permanent_blocked() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + + context.ICCRecordHelper.readICCID = function fakeReadICCID() {}; + + function testPermanentBlocked(pin1_replaced, universalPINState, pin1) { + let iccStatus = { + cardState: CARD_STATE_PRESENT, + gsmUmtsSubscriptionAppIndex: 0, + universalPINState: universalPINState, + apps: [ + { + pin1_replaced: pin1_replaced, + pin1: pin1 + }] + }; + + ril._processICCStatus(iccStatus); + equal(ril.cardState, Ci.nsIIcc.CARD_STATE_PERMANENT_BLOCKED); + } + + testPermanentBlocked(1, + CARD_PINSTATE_ENABLED_PERM_BLOCKED, + CARD_PINSTATE_UNKNOWN); + testPermanentBlocked(1, + CARD_PINSTATE_ENABLED_PERM_BLOCKED, + CARD_PINSTATE_ENABLED_PERM_BLOCKED); + testPermanentBlocked(0, + CARD_PINSTATE_UNKNOWN, + CARD_PINSTATE_ENABLED_PERM_BLOCKED); + + run_next_test(); +}); + +/** + * Verify ICC without app index. + */ +add_test(function test_icc_without_app_index() { + const ICCID = "123456789"; + + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + + let iccStatus = { + cardState: CARD_STATE_PRESENT, + gsmUmtsSubscriptionAppIndex: -1, + universalPINState: CARD_PINSTATE_DISABLED, + apps: [ + { + app_state: CARD_APPSTATE_READY + }] + }; + + context.ICCRecordHelper.readICCID = function fakeReadICCID() { + ril.iccInfo.iccid = ICCID; + }; + + ril._processICCStatus(iccStatus); + + // Should read icc id event if the app index is -1. + equal(ril.iccInfo.iccid, ICCID); + // cardState is "unknown" if the app index is -1. + equal(ril.cardState, GECKO_CARDSTATE_UNKNOWN); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_icc_GsmPDUHelper.js b/dom/system/gonk/tests/test_ril_worker_icc_GsmPDUHelper.js new file mode 100644 index 000000000..0d074da79 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_GsmPDUHelper.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify GsmPDUHelper.writeTimestamp + */ +add_test(function test_write_timestamp() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + + // current date + let dateInput = new Date(); + let dateOutput = new Date(); + helper.writeTimestamp(dateInput); + dateOutput.setTime(helper.readTimestamp()); + + equal(dateInput.getFullYear(), dateOutput.getFullYear()); + equal(dateInput.getMonth(), dateOutput.getMonth()); + equal(dateInput.getDate(), dateOutput.getDate()); + equal(dateInput.getHours(), dateOutput.getHours()); + equal(dateInput.getMinutes(), dateOutput.getMinutes()); + equal(dateInput.getSeconds(), dateOutput.getSeconds()); + equal(dateInput.getTimezoneOffset(), dateOutput.getTimezoneOffset()); + + // 2034-01-23 12:34:56 -0800 GMT + let time = Date.UTC(2034, 1, 23, 12, 34, 56); + time = time - (8 * 60 * 60 * 1000); + dateInput.setTime(time); + helper.writeTimestamp(dateInput); + dateOutput.setTime(helper.readTimestamp()); + + equal(dateInput.getFullYear(), dateOutput.getFullYear()); + equal(dateInput.getMonth(), dateOutput.getMonth()); + equal(dateInput.getDate(), dateOutput.getDate()); + equal(dateInput.getHours(), dateOutput.getHours()); + equal(dateInput.getMinutes(), dateOutput.getMinutes()); + equal(dateInput.getSeconds(), dateOutput.getSeconds()); + equal(dateInput.getTimezoneOffset(), dateOutput.getTimezoneOffset()); + + run_next_test(); +}); + +/** + * Verify GsmPDUHelper.octectToBCD and GsmPDUHelper.BCDToOctet + */ +add_test(function test_octect_BCD() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + + // 23 + let number = 23; + let octet = helper.BCDToOctet(number); + equal(helper.octetToBCD(octet), number); + + // 56 + number = 56; + octet = helper.BCDToOctet(number); + equal(helper.octetToBCD(octet), number); + + // 0x23 + octet = 0x23; + number = helper.octetToBCD(octet); + equal(helper.BCDToOctet(number), octet); + + // 0x56 + octet = 0x56; + number = helper.octetToBCD(octet); + equal(helper.BCDToOctet(number), octet); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_icc_ICCContactHelper.js b/dom/system/gonk/tests/test_ril_worker_icc_ICCContactHelper.js new file mode 100644 index 000000000..29b83b76a --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_ICCContactHelper.js @@ -0,0 +1,1042 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Test error message returned in onerror for readICCContacts. + */ +add_test(function test_error_message_read_icc_contact () { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + + function do_test(options, expectedErrorMsg) { + ril.sendChromeMessage = function(message) { + equal(message.errorMsg, expectedErrorMsg); + } + ril.readICCContacts(options); + } + + // Error 1, didn't specify correct contactType. + do_test({}, CONTACT_ERR_REQUEST_NOT_SUPPORTED); + + // Error 2, specifying a non-supported contactType. + ril.appType = CARD_APPTYPE_USIM; + do_test({contactType: "foo"}, CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + + // Error 3, suppose we update the supported PBR fields in USIM_PBR_FIELDS, + // but forget to add implemenetations for it. + USIM_PBR_FIELDS.push("pbc"); + do_test({contactType: GECKO_CARDCONTACT_TYPE_ADN}, + CONTACT_ERR_FIELD_NOT_SUPPORTED); + + run_next_test(); +}); + +/** + * Test error message returned in onerror for updateICCContact. + */ +add_test(function test_error_message_update_icc_contact() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + + const ICCID = "123456789"; + ril.iccInfo.iccid = ICCID; + + function do_test(options, expectedErrorMsg) { + ril.sendChromeMessage = function(message) { + equal(message.errorMsg, expectedErrorMsg); + } + ril.updateICCContact(options); + } + + // Error 1, didn't specify correct contactType. + do_test({}, CONTACT_ERR_REQUEST_NOT_SUPPORTED); + + // Error 2, specifying a correct contactType, but without providing 'contact'. + do_test({contactType: GECKO_CARDCONTACT_TYPE_ADN}, + CONTACT_ERR_REQUEST_NOT_SUPPORTED); + + // Error 3, specifying a non-supported contactType. + ril.appType = CARD_APPTYPE_USIM; + do_test({contactType: GECKO_CARDCONTACT_TYPE_SDN, contact: {}}, + CONTACT_ERR_CONTACT_TYPE_NOT_SUPPORTED); + + // Error 4, without supplying pin2. + do_test({contactType: GECKO_CARDCONTACT_TYPE_FDN, + contact: {contactId: ICCID + "1"}}, + GECKO_ERROR_SIM_PIN2); + + // Error 5, No free record found in EF_ADN. + let record = context.ICCRecordHelper; + record.readPBR = function(onsuccess, onerror) { + onsuccess([{adn: {fileId: 0x4f3a}}]); + }; + + let io = context.ICCIOHelper; + io.loadLinearFixedEF = function(options) { + options.totalRecords = 1; + options.p1 = 1; + options.callback(options); + }; + + do_test({contactType: GECKO_CARDCONTACT_TYPE_ADN, contact: {}}, + CONTACT_ERR_NO_FREE_RECORD_FOUND); + + // Error 6, ICC IO Error. + io.loadLinearFixedEF = function(options) { + ril[REQUEST_SIM_IO](0, { + errorMsg: GECKO_ERROR_GENERIC_FAILURE + }); + }; + do_test({contactType: GECKO_CARDCONTACT_TYPE_ADN, + contact: {contactId: ICCID + "1"}}, + GECKO_ERROR_GENERIC_FAILURE); + + // Error 7, suppose we update the supported PBR fields in USIM_PBR_FIELDS, + // but forget to add implemenetations for it. + USIM_PBR_FIELDS.push("pbc"); + do_test({contactType: GECKO_CARDCONTACT_TYPE_ADN, + contact: {contactId: ICCID + "1"}}, + CONTACT_ERR_FIELD_NOT_SUPPORTED); + + // Error 8, EF_PBR doesn't exist. + record.readPBR = function(onsuccess, onerror) { + onsuccess([]); + }; + + do_test({contactType: GECKO_CARDCONTACT_TYPE_ADN, + contact: {contactId: ICCID + "1"}}, + CONTACT_ERR_CANNOT_ACCESS_PHONEBOOK); + + run_next_test(); +}); + +/** + * Verify ICCContactHelper.readICCContacts + */ +add_test(function test_read_icc_contacts() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.ICCRecordHelper; + let contactHelper = context.ICCContactHelper; + let ril = context.RIL; + let test_data = [ + //Record 1. + { + comment: "Test read SIM adn contact", + rawData: { + simType: CARD_APPTYPE_SIM, + contactType: GECKO_CARDCONTACT_TYPE_ADN, + adnLike: [{recordId: 1, alphaId: "name", number: "111111"}], + }, + expectedContact: [{ + recordId: 1, + alphaId: "name", + number: "111111" + }], + }, + //Record 2. + { + comment: "Test read SIM fdn contact", + rawData: { + simType: CARD_APPTYPE_SIM, + contactType: GECKO_CARDCONTACT_TYPE_FDN, + adnLike: [{recordId: 1, alphaId: "name", number: "111111"}], + }, + expectedContact: [{ + recordId: 1, + alphaId: "name", + number: "111111" + }], + }, + //Record 3. + { + comment: "Test read USIM adn contact", + rawData: { + simType: CARD_APPTYPE_USIM, + contactType: GECKO_CARDCONTACT_TYPE_ADN, + pbrs: [{adn:{fileId: 0x6f3a}, email: {}, anr0: {}}], + adnLike: [{recordId: 1, alphaId: "name", number: "111111"}], + email: "hello@mail.com", + anr: "123456", + }, + expectedContact: [{ + pbrIndex: 0, + recordId: 1, + alphaId: "name", + number: "111111", + email: "hello@mail.com", + anr: ["123456"] + }], + }, + //Record 4. + { + comment: "Test read USIM adn contacts", + rawData: { + simType: CARD_APPTYPE_USIM, + contactType: GECKO_CARDCONTACT_TYPE_ADN, + pbrs: [{adn:{fileId: 0x6f3a}, email: {}, anr0: {}}, + {adn:{fileId: 0x6f3b}, email: {}, anr0: {}}], + adnLike: [{recordId: 1, alphaId: "name1", number: "111111"}, + {recordId: 2, alphaId: "name2", number: "222222"}], + email: "hello@mail.com", + anr: "123456", + }, + expectedContact: [ + { + pbrIndex: 0, + recordId: 1, + alphaId: "name1", + number: "111111", + email: "hello@mail.com", + anr: ["123456"] + }, { + pbrIndex: 0, + recordId: 2, + alphaId: "name2", + number: "222222", + email: "hello@mail.com", + anr: ["123456"] + }, { + pbrIndex: 1, + recordId: 1, + alphaId: "name1", + number: "111111", + email: "hello@mail.com", + anr: ["123456"] + }, { + pbrIndex: 1, + recordId: 2, + alphaId: "name2", + number: "222222", + email: "hello@mail.com", + anr: ["123456"] + } + ], + }, + //Record 5. + { + comment: "Test read USIM fdn contact", + rawData: { + simType: CARD_APPTYPE_USIM, + contactType: GECKO_CARDCONTACT_TYPE_FDN, + adnLike: [{recordId: 1, alphaId: "name", number: "111111"}], + }, + expectedContact: [{ + recordId: 1, + alphaId: "name", + number: "111111" + }], + }, + //Record 6. + { + comment: "Test read RUIM adn contact", + rawData: { + simType: CARD_APPTYPE_RUIM, + contactType: GECKO_CARDCONTACT_TYPE_ADN, + adnLike: [{recordId: 1, alphaId: "name", number: "111111"}], + }, + expectedContact: [{ + recordId: 1, + alphaId: "name", + number: "111111" + }], + }, + //Record 7. + { + comment: "Test read RUIM fdn contact", + rawData: { + simType: CARD_APPTYPE_RUIM, + contactType: GECKO_CARDCONTACT_TYPE_FDN, + adnLike: [{recordId: 1, alphaId: "name", number: "111111"}], + }, + expectedContact: [{ + recordId: 1, + alphaId: "name", + number: "111111" + }], + }, + //Record 8. + { + comment: "Test read RUIM adn contact with enhanced phone book", + rawData: { + simType: CARD_APPTYPE_RUIM, + contactType: GECKO_CARDCONTACT_TYPE_ADN, + pbrs: [{adn:{fileId: 0x6f3a}, email: {}, anr0: {}}], + adnLike: [{recordId: 1, alphaId: "name", number: "111111"}], + email: "hello@mail.com", + anr: "123456", + enhancedPhoneBook: true, + }, + expectedContact: [{ + pbrIndex: 0, + recordId: 1, + alphaId: "name", + number: "111111", + email: "hello@mail.com", + anr: ["123456"] + }], + }, + //Record 9. + { + comment: "Test read RUIM adn contacts with enhanced phone book", + rawData: { + simType: CARD_APPTYPE_RUIM, + contactType: GECKO_CARDCONTACT_TYPE_ADN, + pbrs: [{adn:{fileId: 0x6f3a}, email: {}, anr0: {}}, + {adn:{fileId: 0x6f3b}, email: {}, anr0: {}}], + adnLike: [{recordId: 1, alphaId: "name1", number: "111111"}, + {recordId: 2, alphaId: "name2", number: "222222"}], + email: "hello@mail.com", + anr: "123456", + enhancedPhoneBook: true, + }, + expectedContact: [ + { + pbrIndex: 0, + recordId: 1, + alphaId: "name1", + number: "111111", + email: "hello@mail.com", + anr: ["123456"] + }, { + pbrIndex: 0, + recordId: 2, + alphaId: "name2", + number: "222222", + email: "hello@mail.com", + anr: ["123456"] + }, { + pbrIndex: 1, + recordId: 1, + alphaId: "name1", + number: "111111", + email: "hello@mail.com", + anr: ["123456"] + }, { + pbrIndex: 1, + recordId: 2, + alphaId: "name2", + number: "222222", + email: "hello@mail.com", + anr: ["123456"] + } + ], + }, + ]; + + function do_test(aTestData, aExpectedContact) { + ril.appType = aTestData.simType; + ril._isCdma = (aTestData.simType === CARD_APPTYPE_RUIM); + ril.iccInfoPrivate.cst = (aTestData.enhancedPhoneBook) ? + [0x20, 0x0C, 0x0, 0x0, 0x0]: + [0x20, 0x00, 0x0, 0x0, 0x0]; + + ril.iccInfoPrivate.sst = (aTestData.simType === CARD_APPTYPE_SIM)? + [0x20, 0x0, 0x0, 0x0, 0x0]: + [0x2, 0x0, 0x0, 0x0, 0x0]; + + // Override some functions to test. + contactHelper.getContactFieldRecordId = function(pbr, contact, field, onsuccess, onerror) { + onsuccess(1); + }; + + record.readPBR = function readPBR(onsuccess, onerror) { + onsuccess(JSON.parse(JSON.stringify(aTestData.pbrs))); + }; + + record.readADNLike = function readADNLike(fileId, extFileId, onsuccess, onerror) { + onsuccess(JSON.parse(JSON.stringify(aTestData.adnLike))); + }; + + record.readEmail = function readEmail(fileId, fileType, recordNumber, onsuccess, onerror) { + onsuccess(aTestData.email); + }; + + record.readANR = function readANR(fileId, fileType, recordNumber, onsuccess, onerror) { + onsuccess(aTestData.anr); + }; + + let onsuccess = function onsuccess(contacts) { + for (let i = 0; i < contacts.length; i++) { + do_print("check contacts[" + i + "]:" + JSON.stringify(contacts[i])); + deepEqual(contacts[i], aExpectedContact[i]); + } + }; + + let onerror = function onerror(errorMsg) { + do_print("readICCContacts failed: " + errorMsg); + ok(false); + }; + + contactHelper.readICCContacts(aTestData.simType, aTestData.contactType, onsuccess, onerror); + } + + for (let i = 0; i < test_data.length; i++) { + do_print(test_data[i].comment); + do_test(test_data[i].rawData, test_data[i].expectedContact); + } + + run_next_test(); +}); + +/** + * Verify ICCContactHelper.updateICCContact with appType is CARD_APPTYPE_USIM. + */ +add_test(function test_update_icc_contact() { + const ADN_RECORD_ID = 100; + const ADN_SFI = 1; + const IAP_FILE_ID = 0x4f17; + const EMAIL_FILE_ID = 0x4f50; + const EMAIL_RECORD_ID = 20; + const ANR0_FILE_ID = 0x4f11; + const ANR0_RECORD_ID = 30; + const EXT_RECORD_ID = 0x01; + + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let recordHelper = context.ICCRecordHelper; + let contactHelper = context.ICCContactHelper; + let ril = context.RIL; + + function do_test(aSimType, aContactType, aContact, aPin2, aFileType, aHaveIapIndex, aEnhancedPhoneBook) { + ril.appType = aSimType; + ril._isCdma = (aSimType === CARD_APPTYPE_RUIM); + ril.iccInfoPrivate.cst = (aEnhancedPhoneBook) ? [0x20, 0x0C, 0x28, 0x0, 0x20] + : [0x20, 0x0, 0x28, 0x0, 0x20]; + ril.iccInfoPrivate.sst = (aSimType === CARD_APPTYPE_SIM)? + [0x20, 0x0, 0x28, 0x0, 0x20]: + [0x16, 0x0, 0x0, 0x0, 0x0]; + + recordHelper.readPBR = function(onsuccess, onerror) { + if (aFileType === ICC_USIM_TYPE1_TAG) { + onsuccess([{ + adn: {fileId: ICC_EF_ADN}, + email: {fileId: EMAIL_FILE_ID, + fileType: ICC_USIM_TYPE1_TAG}, + anr0: {fileId: ANR0_FILE_ID, + fileType: ICC_USIM_TYPE1_TAG}, + ext1: {fileId: ICC_EF_EXT1} + + }]); + } else if (aFileType === ICC_USIM_TYPE2_TAG) { + onsuccess([{ + adn: {fileId: ICC_EF_ADN, + sfi: ADN_SFI}, + iap: {fileId: IAP_FILE_ID}, + email: {fileId: EMAIL_FILE_ID, + fileType: ICC_USIM_TYPE2_TAG, + indexInIAP: 0}, + anr0: {fileId: ANR0_FILE_ID, + fileType: ICC_USIM_TYPE2_TAG, + indexInIAP: 1}, + ext1: {fileId: ICC_EF_EXT1} + }]); + } + }; + + recordHelper.updateADNLike = function(fileId, extRecordNumber, contact, pin2, onsuccess, onerror) { + if (aContactType === GECKO_CARDCONTACT_TYPE_FDN) { + equal(fileId, ICC_EF_FDN); + } else if (aContactType === GECKO_CARDCONTACT_TYPE_ADN) { + equal(fileId, ICC_EF_ADN); + } + + if (aContact.number.length > ADN_MAX_NUMBER_DIGITS) { + equal(extRecordNumber, EXT_RECORD_ID); + } else { + equal(extRecordNumber, 0xff); + } + + equal(pin2, aPin2); + equal(contact.alphaId, aContact.alphaId); + equal(contact.number, aContact.number); + onsuccess({alphaId: contact.alphaId, + number: contact.number.substring(0, ADN_MAX_NUMBER_DIGITS)}); + }; + + recordHelper.getADNLikeExtensionRecordNumber = function(fileId, recordNumber, onsuccess, onerror) { + onsuccess(EXT_RECORD_ID); + }; + + recordHelper.updateExtension = function(fileId, recordNumber, number, onsuccess, onerror) { + onsuccess(); + }; + + recordHelper.findFreeRecordId = function(fileId, onsuccess, onerror) { + onsuccess(EXT_RECORD_ID); + }; + + recordHelper.cleanEFRecord = function(fileId, recordNumber, onsuccess, onerror) { + onsuccess(); + } + + recordHelper.readIAP = function(fileId, recordNumber, onsuccess, onerror) { + equal(fileId, IAP_FILE_ID); + equal(recordNumber, ADN_RECORD_ID); + onsuccess((aHaveIapIndex) ? [EMAIL_RECORD_ID, ANR0_RECORD_ID] + : [0xff, 0xff]); + }; + + recordHelper.updateIAP = function(fileId, recordNumber, iap, onsuccess, onerror) { + equal(fileId, IAP_FILE_ID); + equal(recordNumber, ADN_RECORD_ID); + onsuccess(); + }; + + recordHelper.updateEmail = function(pbr, recordNumber, email, adnRecordId, onsuccess, onerror) { + equal(pbr.email.fileId, EMAIL_FILE_ID); + if (pbr.email.fileType === ICC_USIM_TYPE1_TAG) { + equal(recordNumber, ADN_RECORD_ID); + } else if (pbr.email.fileType === ICC_USIM_TYPE2_TAG) { + equal(recordNumber, EMAIL_RECORD_ID); + } + equal(email, aContact.email); + onsuccess(email); + }; + + recordHelper.updateANR = function(pbr, recordNumber, number, adnRecordId, onsuccess, onerror) { + equal(pbr.anr0.fileId, ANR0_FILE_ID); + if (pbr.anr0.fileType === ICC_USIM_TYPE1_TAG) { + equal(recordNumber, ADN_RECORD_ID); + } else if (pbr.anr0.fileType === ICC_USIM_TYPE2_TAG) { + equal(recordNumber, ANR0_RECORD_ID); + } + if (Array.isArray(aContact.anr)) { + equal(number, aContact.anr[0]); + } + onsuccess(number); + }; + + recordHelper.findFreeRecordId = function(fileId, onsuccess, onerror) { + let recordId = 0; + if (fileId === EMAIL_FILE_ID) { + recordId = EMAIL_RECORD_ID; + } else if (fileId === ANR0_FILE_ID) { + recordId = ANR0_RECORD_ID; + } + onsuccess(recordId); + }; + + let isSuccess = false; + let onsuccess = function onsuccess(updatedContact) { + equal(ADN_RECORD_ID, updatedContact.recordId); + equal(aContact.alphaId, updatedContact.alphaId); + equal(aContact.number.substring(0, ADN_MAX_NUMBER_DIGITS + EXT_MAX_NUMBER_DIGITS), + updatedContact.number); + if ((aSimType == CARD_APPTYPE_USIM || aSimType == CARD_APPTYPE_RUIM) && + (aFileType == ICC_USIM_TYPE1_TAG || aFileType == ICC_USIM_TYPE2_TAG)) { + if (aContact.hasOwnProperty('email')) { + equal(aContact.email, updatedContact.email); + } + + if (aContact.hasOwnProperty('anr')) { + equal(aContact.anr[0], updatedContact.anr[0]); + } + } else { + equal(updatedContact.email, null); + equal(updatedContact.anr, null); + } + + do_print("updateICCContact success"); + isSuccess = true; + }; + + let onerror = function onerror(errorMsg) { + do_print("updateICCContact failed: " + errorMsg); + }; + + contactHelper.updateICCContact(aSimType, aContactType, aContact, aPin2, onsuccess, onerror); + ok(isSuccess); + } + + let contacts = [ + { + pbrIndex: 0, + recordId: ADN_RECORD_ID, + alphaId: "test", + number: "123456", + email: "test@mail.com", + anr: ["+654321"] + }, + // a contact without email and anr. + { + pbrIndex: 0, + recordId: ADN_RECORD_ID, + alphaId: "test2", + number: "123456", + }, + // a contact with email but no anr. + { + pbrIndex: 0, + recordId: ADN_RECORD_ID, + alphaId: "test3", + number: "123456", + email: "test@mail.com" + }, + // a contact with anr but no email. + { + pbrIndex: 0, + recordId: ADN_RECORD_ID, + alphaId: "test4", + number: "123456", + anr: ["+654321"] + }, + // a contact number over 20 digits. + { + pbrIndex: 0, + recordId: ADN_RECORD_ID, + alphaId: "test4", + number: "0123456789012345678901234567890123456789", + anr: ["+654321"] + }, + // a contact number over 40 digits. + { + pbrIndex: 0, + recordId: ADN_RECORD_ID, + alphaId: "test5", + number: "01234567890123456789012345678901234567890123456789", + anr: ["+654321"] + }]; + + for (let i = 0; i < contacts.length; i++) { + let contact = contacts[i]; + // SIM + do_print("Test update SIM adn contacts"); + do_test(CARD_APPTYPE_SIM, GECKO_CARDCONTACT_TYPE_ADN, contact); + + do_print("Test update SIM fdn contacts"); + do_test(CARD_APPTYPE_SIM, GECKO_CARDCONTACT_TYPE_FDN, contact, "1234"); + + // USIM + do_print("Test update USIM adn contacts"); + do_test(CARD_APPTYPE_USIM, GECKO_CARDCONTACT_TYPE_ADN, contact, null, + ICC_USIM_TYPE1_TAG); + do_test(CARD_APPTYPE_USIM, GECKO_CARDCONTACT_TYPE_ADN, contact, null, + ICC_USIM_TYPE2_TAG, true); + do_test(CARD_APPTYPE_USIM, GECKO_CARDCONTACT_TYPE_ADN, contact, null, + ICC_USIM_TYPE2_TAG, false); + + do_print("Test update USIM fdn contacts"); + do_test(CARD_APPTYPE_USIM, GECKO_CARDCONTACT_TYPE_FDN, contact, "1234"); + + // RUIM + do_print("Test update RUIM adn contacts"); + do_test(CARD_APPTYPE_RUIM, GECKO_CARDCONTACT_TYPE_ADN, contact); + + do_print("Test update RUIM fdn contacts"); + do_test(CARD_APPTYPE_RUIM, GECKO_CARDCONTACT_TYPE_FDN, contact, "1234"); + + // RUIM with enhanced phone book + do_print("Test update RUIM adn contacts with enhanced phone book"); + do_test(CARD_APPTYPE_RUIM, GECKO_CARDCONTACT_TYPE_ADN, contact, null, + ICC_USIM_TYPE1_TAG, null,true); + do_test(CARD_APPTYPE_RUIM, GECKO_CARDCONTACT_TYPE_ADN, contact, null, + ICC_USIM_TYPE2_TAG, true, true); + do_test(CARD_APPTYPE_RUIM, GECKO_CARDCONTACT_TYPE_ADN, contact, null, + ICC_USIM_TYPE2_TAG, false, true); + + do_print("Test update RUIM fdn contacts with enhanced phone book"); + do_test(CARD_APPTYPE_RUIM, GECKO_CARDCONTACT_TYPE_FDN, contact, "1234", + null, true); + } + + run_next_test(); +}); + +/** + * Verify ICCContactHelper.updateICCContact with appType is CARD_APPTYPE_USIM and + * insufficient space to store Type 2 USIM contact fields. + */ +add_test(function test_update_icc_contact_full_email_and_anr_field() { + const ADN_RECORD_ID = 100; + const ADN_SFI = 1; + const IAP_FILE_ID = 0x4f17; + const EMAIL_FILE_ID = 0x4f50; + const EMAIL_RECORD_ID = 20; + const ANR0_FILE_ID = 0x4f11; + const ANR0_RECORD_ID = 30; + + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let recordHelper = context.ICCRecordHelper; + let contactHelper = context.ICCContactHelper; + let ril = context.RIL; + + function do_test(aSimType, aContactType, aContact, aPin2) { + ril.appType = CARD_APPTYPE_USIM; + ril.iccInfoPrivate.sst = [0x2, 0x0, 0x0, 0x0, 0x0]; + + recordHelper.readPBR = function(onsuccess, onerror) { + onsuccess([{ + adn: {fileId: ICC_EF_ADN, + sfi: ADN_SFI}, + iap: {fileId: IAP_FILE_ID}, + email: {fileId: EMAIL_FILE_ID, + fileType: ICC_USIM_TYPE2_TAG, + indexInIAP: 0}, + anr0: {fileId: ANR0_FILE_ID, + fileType: ICC_USIM_TYPE2_TAG, + indexInIAP: 1} + }]); + }; + + recordHelper.updateADNLike = function(fileId, extRecordNumber, contact, pin2, onsuccess, onerror) { + if (aContactType === GECKO_CARDCONTACT_TYPE_ADN) { + equal(fileId, ICC_EF_ADN); + } + equal(pin2, aPin2); + equal(contact.alphaId, aContact.alphaId); + equal(contact.number, aContact.number); + onsuccess({alphaId: contact.alphaId, + number: contact.number}); + }; + + recordHelper.readIAP = function(fileId, recordNumber, onsuccess, onerror) { + equal(fileId, IAP_FILE_ID); + equal(recordNumber, ADN_RECORD_ID); + onsuccess([0xff, 0xff]); + }; + + recordHelper.updateIAP = function(fileId, recordNumber, iap, onsuccess, onerror) { + equal(fileId, IAP_FILE_ID); + equal(recordNumber, ADN_RECORD_ID); + onsuccess(); + }; + + recordHelper.findFreeRecordId = function(fileId, onsuccess, onerror) { + let recordId = 0; + // emulate email and anr don't have free record. + if (fileId === EMAIL_FILE_ID || fileId === ANR0_FILE_ID) { + onerror(CONTACT_ERR_NO_FREE_RECORD_FOUND); + } else { + onsuccess(recordId); + } + }; + + let isSuccess = false; + let onsuccess = function onsuccess(updatedContact) { + equal(ADN_RECORD_ID, updatedContact.recordId); + equal(aContact.alphaId, updatedContact.alphaId); + equal(updatedContact.email, null); + equal(updatedContact.anr, null); + + do_print("updateICCContact success"); + isSuccess = true; + }; + + let onerror = function onerror(errorMsg) { + do_print("updateICCContact failed: " + errorMsg); + }; + + contactHelper.updateICCContact(aSimType, aContactType, aContact, aPin2, onsuccess, onerror); + ok(isSuccess); + } + + let contact = { + pbrIndex: 0, + recordId: ADN_RECORD_ID, + alphaId: "test", + number: "123456", + email: "test@mail.com", + anr: ["+654321"] + }; + + // USIM + do_print("Test update USIM adn contacts"); + do_test(CARD_APPTYPE_USIM, GECKO_CARDCONTACT_TYPE_ADN, contact, null); + + run_next_test(); +}); + +/** + * Verify updateICCContact with removal of anr and email with File Type 1. + */ +add_test(function test_update_icc_contact_with_remove_type1_attr() { + const ADN_RECORD_ID = 100; + const IAP_FILE_ID = 0x4f17; + const EMAIL_FILE_ID = 0x4f50; + const EMAIL_RECORD_ID = 20; + const ANR0_FILE_ID = 0x4f11; + const ANR0_RECORD_ID = 30; + + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let recordHelper = context.ICCRecordHelper; + let contactHelper = context.ICCContactHelper; + + recordHelper.updateADNLike = function(fileId, extRecordNumber, contact, pin2, onsuccess, onerror) { + onsuccess({alphaId: contact.alphaId, + number: contact.number}); + }; + + let contact = { + pbrIndex: 0, + recordId: ADN_RECORD_ID, + alphaId: "test2", + number: "123456", + }; + + recordHelper.readIAP = function(fileId, recordNumber, onsuccess, onerror) { + onsuccess([EMAIL_RECORD_ID, ANR0_RECORD_ID]); + }; + + recordHelper.updateEmail = function(pbr, recordNumber, email, adnRecordId, onsuccess, onerror) { + ok(email == null); + onsuccess(email); + }; + + recordHelper.updateANR = function(pbr, recordNumber, number, adnRecordId, onsuccess, onerror) { + ok(number == null); + onsuccess(number); + }; + + function do_test(type) { + recordHelper.readPBR = function(onsuccess, onerror) { + if (type == ICC_USIM_TYPE1_TAG) { + onsuccess([{ + adn: {fileId: ICC_EF_ADN}, + email: {fileId: EMAIL_FILE_ID, + fileType: ICC_USIM_TYPE1_TAG}, + anr0: {fileId: ANR0_FILE_ID, + fileType: ICC_USIM_TYPE1_TAG}}]); + } else { + onsuccess([{ + adn: {fileId: ICC_EF_ADN}, + iap: {fileId: IAP_FILE_ID}, + email: {fileId: EMAIL_FILE_ID, + fileType: ICC_USIM_TYPE2_TAG, + indexInIAP: 0}, + anr0: {fileId: ANR0_FILE_ID, + fileType: ICC_USIM_TYPE2_TAG, + indexInIAP: 1}}]); + } + }; + + let successCb = function(updatedContact) { + equal(updatedContact.email, null); + equal(updatedContact.anr, null); + ok(true); + }; + + let errorCb = function(errorMsg) { + do_print(errorMsg); + ok(false); + }; + + contactHelper.updateICCContact(CARD_APPTYPE_USIM, + GECKO_CARDCONTACT_TYPE_ADN, + contact, null, successCb, errorCb); + } + + do_test(ICC_USIM_TYPE1_TAG); + do_test(ICC_USIM_TYPE2_TAG); + + run_next_test(); +}); + +/** + * Verify ICCContactHelper.findFreeICCContact in SIM + */ +add_test(function test_find_free_icc_contact_sim() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let recordHelper = context.ICCRecordHelper; + let contactHelper = context.ICCContactHelper; + // Correct record Id starts with 1, so put a null element at index 0. + let records = [null]; + const MAX_RECORDS = 3; + const PBR_INDEX = 0; + + recordHelper.findFreeRecordId = function(fileId, onsuccess, onerror) { + if (records.length > MAX_RECORDS) { + onerror("No free record found."); + return; + } + + onsuccess(records.length); + }; + + let successCb = function(pbrIndex, recordId) { + equal(pbrIndex, PBR_INDEX); + records[recordId] = {}; + }; + + let errorCb = function(errorMsg) { + do_print(errorMsg); + ok(false); + }; + + for (let i = 0; i < MAX_RECORDS; i++) { + contactHelper.findFreeICCContact(CARD_APPTYPE_SIM, + GECKO_CARDCONTACT_TYPE_ADN, + successCb, errorCb); + } + // The 1st element, records[0], is null. + equal(records.length - 1, MAX_RECORDS); + + // Now the EF is full, so finding a free one should result failure. + successCb = function(pbrIndex, recordId) { + ok(false); + }; + + errorCb = function(errorMsg) { + ok(errorMsg === "No free record found."); + }; + contactHelper.findFreeICCContact(CARD_APPTYPE_SIM, GECKO_CARDCONTACT_TYPE_ADN, + successCb, errorCb); + + run_next_test(); +}); + +/** + * Verify ICCContactHelper.findFreeICCContact in USIM + */ +add_test(function test_find_free_icc_contact_usim() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let recordHelper = context.ICCRecordHelper; + let contactHelper = context.ICCContactHelper; + const ADN1_FILE_ID = 0x6f3a; + const ADN2_FILE_ID = 0x6f3b; + const MAX_RECORDS = 3; + + // The adn in the first phonebook set has already two records, which means + // only 1 free record remained. + let pbrs = [{adn: {fileId: ADN1_FILE_ID, records: [null, {}, {}]}}, + {adn: {fileId: ADN2_FILE_ID, records: [null]}}]; + + recordHelper.readPBR = function readPBR(onsuccess, onerror) { + onsuccess(pbrs); + }; + + recordHelper.findFreeRecordId = function(fileId, onsuccess, onerror) { + let pbr = (fileId == ADN1_FILE_ID ? pbrs[0]: pbrs[1]); + if (pbr.adn.records.length > MAX_RECORDS) { + onerror("No free record found."); + return; + } + + onsuccess(pbr.adn.records.length); + }; + + let successCb = function(pbrIndex, recordId) { + equal(pbrIndex, 0); + pbrs[pbrIndex].adn.records[recordId] = {}; + }; + + let errorCb = function(errorMsg) { + ok(false); + }; + + contactHelper.findFreeICCContact(CARD_APPTYPE_USIM, + GECKO_CARDCONTACT_TYPE_ADN, + successCb, errorCb); + + // Now the EF_ADN in the 1st phonebook set is full, so the next free contact + // will come from the 2nd phonebook set. + successCb = function(pbrIndex, recordId) { + equal(pbrIndex, 1); + equal(recordId, 1); + } + contactHelper.findFreeICCContact(CARD_APPTYPE_USIM, + GECKO_CARDCONTACT_TYPE_ADN, + successCb, errorCb); + + run_next_test(); +}); + +/** + * Verify ICCContactHelper.updateADNLikeWithExtension + */ +add_test(function test_update_adn_like_with_extension() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let record = context.ICCRecordHelper; + let contactHelper = context.ICCContactHelper; + ril.appType = CARD_APPTYPE_SIM; + // Correct record Id starts from 1, so put a null element at index 0. + // ext_records contains data at index 1, and it only has 1 free record at index 2. + let notFree = 0x01; + let ext_records = [null, notFree, null]; + + function do_test(contact, extRecordNumber, expectedExtRecordNumber, expectedNumber, expectedCleanEFRecord) { + // Override some functions to test. + record.getADNLikeExtensionRecordNumber = function(fileId, recordNumber, onsuccess, onerror) { + onsuccess(extRecordNumber); + } + + record.updateADNLike = function(fileId, extRecordNumber, contact, pin2, onsuccess, onerror) { + equal(extRecordNumber, expectedExtRecordNumber); + onsuccess({alphaId: contact.alphaId, + number: contact.number.substring(0, ADN_MAX_NUMBER_DIGITS)}); + } + + record.updateExtension = function(fileId, recordNumber, number, onsuccess, onerror) { + if (recordNumber > ext_records.length) { + onerror("updateExtension failed."); + return; + } + ext_records[recordNumber] = number; + onsuccess(); + } + + record.findFreeRecordId = function(fileId, onsuccess, onerror) { + for (let i = 1; i < ext_records.length; i++) { + if (!ext_records[i]) { + onsuccess(i); + return; + } + } + + onerror("No free record found."); + } + + let isCleanEFRecord = false; + record.cleanEFRecord = function(fileId, recordNumber, onsuccess, onerror) { + if (recordNumber > ext_records.length) { + onerror("cleanEFRecord failed."); + return; + } + ext_records[recordNumber] = null; + isCleanEFRecord = true; + onsuccess(); + } + + let successCb = function successCb(updatedContact) { + equal(updatedContact.number, expectedNumber); + }; + + let errorCb = function errorCb(errorMsg) { + do_print("updateADNLikeWithExtension failed, msg = " + errorMsg); + ok(false); + }; + + contactHelper.updateADNLikeWithExtension(ICC_EF_ADN, ICC_EF_EXT1, contact, null, successCb, errorCb); + + if (expectedCleanEFRecord) { + ok(isCleanEFRecord); + } + } + + // Update extension record with previous extension record number. + do_test({recordId: 1, alphaId: "test", number: "001122334455667788991234"}, 0x01, 0x01, "001122334455667788991234"); + // Update extension record and find a free record. + do_test({recordId: 1, alphaId: "test", number: "001122334455667788995678"}, 0xff, 0x02, "001122334455667788995678"); + // Update extension record with no free extension record. + do_test({recordId: 1, alphaId: "test", number: "001122334455667788994321"}, 0xff, 0xff, "00112233445566778899"); + // Update extension record with clean previous extension record. + do_test({recordId: 1, alphaId: "test", number: "00112233445566778899"}, 0x01, 0xff, "00112233445566778899", true); + // Update extension record with no extension record and previous extension record. + do_test({recordId: 1, alphaId: "test", number: "00112233445566778899"}, 0xff, 0xff, "00112233445566778899"); + + run_next_test(); +});
\ No newline at end of file diff --git a/dom/system/gonk/tests/test_ril_worker_icc_ICCIOHelper.js b/dom/system/gonk/tests/test_ril_worker_icc_ICCIOHelper.js new file mode 100644 index 000000000..e690b1206 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_ICCIOHelper.js @@ -0,0 +1,173 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify ICCIOHelper.loadLinearFixedEF with recordSize. + */ +add_test(function test_load_linear_fixed_ef() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let io = context.ICCIOHelper; + + io.getResponse = function fakeGetResponse(options) { + // When recordSize is provided, loadLinearFixedEF should call iccIO directly. + ok(false); + run_next_test(); + }; + + ril.iccIO = function fakeIccIO(options) { + ok(true); + run_next_test(); + }; + + io.loadLinearFixedEF({recordSize: 0x20}); +}); + +/** + * Verify ICCIOHelper.loadLinearFixedEF without recordSize. + */ +add_test(function test_load_linear_fixed_ef() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let io = context.ICCIOHelper; + + io.getResponse = function fakeGetResponse(options) { + ok(true); + run_next_test(); + }; + + ril.iccIO = function fakeIccIO(options) { + // When recordSize is not provided, loadLinearFixedEF should call getResponse. + ok(false); + run_next_test(); + }; + + io.loadLinearFixedEF({}); +}); + +/** + * Verify ICC IO Error. + */ +add_test(function test_process_icc_io_error() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + + function do_test(sw1, sw2, expectedErrorMsg) { + let called = false; + function errorCb(errorMsg) { + called = true; + equal(errorMsg, expectedErrorMsg); + } + + // Write sw1 and sw2 to buffer. + buf.writeInt32(sw1); + buf.writeInt32(sw2); + + context.RIL[REQUEST_SIM_IO](0, {fileId: 0xffff, + command: 0xff, + onerror: errorCb}); + + // onerror callback should be triggered. + ok(called); + } + + let TEST_DATA = [ + // [sw1, sw2, expectError] + [ICC_STATUS_ERROR_COMMAND_NOT_ALLOWED, 0xff, GECKO_ERROR_GENERIC_FAILURE], + [ICC_STATUS_ERROR_WRONG_PARAMETERS, 0xff, GECKO_ERROR_GENERIC_FAILURE], + ]; + + for (let i = 0; i < TEST_DATA.length; i++) { + do_test.apply(null, TEST_DATA[i]); + } + + run_next_test(); +}); + +/** + * Verify ICCIOHelper.processICCIOGetResponse for EF_TYPE_TRANSPARENT. + */ +add_test(function test_icc_io_get_response_for_transparent_structure() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let iccioHelper = context.ICCIOHelper; + let pduHelper = context.GsmPDUHelper; + + let responseArray = [ + // SIM response. + [0x00, 0x00, 0x00, 0x0A, 0x2F, 0xE2, 0x04, 0x00, 0x0A, 0xA0, 0xAA, 0x00, + 0x02, 0x00, 0x00], + // USIM response. + [0x62, 0x22, 0x82, 0x02, 0x41, 0x21, 0x83, 0x02, 0x2F, 0xE2, 0xA5, 0x09, + 0xC1, 0x04, 0x40, 0x0F, 0xF5, 0x55, 0x92, 0x01, 0x00, 0x8A, 0x01, 0x05, + 0x8B, 0x03, 0x2F, 0x06, 0x0B, 0x80, 0x02, 0x00, 0x0A, 0x88, 0x01, 0x10] + ]; + + for (let i = 0; i < responseArray.length; i++) { + let strLen = responseArray[i].length * 2; + buf.writeInt32(strLen); + for (let j = 0; j < responseArray[i].length; j++) { + pduHelper.writeHexOctet(responseArray[i][j]); + } + buf.writeStringDelimiter(strLen); + + let options = {fileId: ICC_EF_ICCID, + structure: EF_STRUCTURE_TRANSPARENT}; + iccioHelper.processICCIOGetResponse(options); + + equal(options.fileSize, 0x0A); + } + + run_next_test(); +}); + +/** + * Verify ICCIOHelper.processICCIOGetResponse for EF_TYPE_LINEAR_FIXED. + */ +add_test(function test_icc_io_get_response_for_linear_fixed_structure() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let iccioHelper = context.ICCIOHelper; + let pduHelper = context.GsmPDUHelper; + + let responseArray = [ + // SIM response. + [0x00, 0x00, 0x00, 0x1A, 0x6F, 0x40, 0x04, 0x00, 0x11, 0xA0, 0xAA, 0x00, + 0x02, 0x01, 0x1A], + // USIM response. + [0x62, 0x1E, 0x82, 0x05, 0x42, 0x21, 0x00, 0x1A, 0x01, 0x83, 0x02, 0x6F, + 0x40, 0xA5, 0x03, 0x92, 0x01, 0x00, 0x8A, 0x01, 0x07, 0x8B, 0x03, 0x6F, + 0x06, 0x02, 0x80, 0x02, 0x00, 0x1A, 0x88, 0x00] + ]; + + for (let i = 0; i < responseArray.length; i++) { + let strLen = responseArray[i].length * 2; + buf.writeInt32(strLen); + for (let j = 0; j < responseArray[i].length; j++) { + pduHelper.writeHexOctet(responseArray[i][j]); + } + buf.writeStringDelimiter(strLen); + + let options = {fileId: ICC_EF_MSISDN, + structure: EF_STRUCTURE_LINEAR_FIXED}; + iccioHelper.processICCIOGetResponse(options); + + equal(options.fileSize, 0x1A); + equal(options.recordSize, 0x1A); + equal(options.totalRecords, 0x01); + } + + run_next_test(); +}); + diff --git a/dom/system/gonk/tests/test_ril_worker_icc_ICCPDUHelper.js b/dom/system/gonk/tests/test_ril_worker_icc_ICCPDUHelper.js new file mode 100644 index 000000000..91495b1b7 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_ICCPDUHelper.js @@ -0,0 +1,652 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify ICCPDUHelper#readICCUCS2String() + */ +add_test(function test_read_icc_ucs2_string() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + + // 0x80 + let text = "TEST"; + helper.writeUCS2String(text); + // Also write two unused octets. + let ffLen = 2; + for (let i = 0; i < ffLen; i++) { + helper.writeHexOctet(0xff); + } + equal(iccHelper.readICCUCS2String(0x80, (2 * text.length) + ffLen), text); + + // 0x81 + let array = [0x08, 0xd2, 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, 0xca, + 0xff, 0xff]; + let len = array.length; + for (let i = 0; i < len; i++) { + helper.writeHexOctet(array[i]); + } + equal(iccHelper.readICCUCS2String(0x81, len), "Mozilla\u694a"); + + // 0x82 + let array2 = [0x08, 0x69, 0x00, 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, + 0xca, 0xff, 0xff]; + let len2 = array2.length; + for (let i = 0; i < len2; i++) { + helper.writeHexOctet(array2[i]); + } + equal(iccHelper.readICCUCS2String(0x82, len2), "Mozilla\u694a"); + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper#writeICCUCS2String() + */ +add_test(function test_write_icc_ucs2_string() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + let alphaLen = 18; + let test_data = [ + { + encode: 0x80, + // string only contain one character. + data: "\u82b3" + }, { + encode: 0x80, + // 2 UCS2 character not located in the same half-page. + data: "Fire \u82b3\u8233" + }, { + encode: 0x80, + // 2 UCS2 character not located in the same half-page. + data: "\u694a\u704a" + }, { + encode: 0x81, + // 2 UCS2 character within same half-page. + data: "Fire \u6901\u697f" + }, { + encode: 0x81, + // 2 UCS2 character within same half-page. + data: "Fire \u6980\u69ff" + }, { + encode: 0x82, + // 2 UCS2 character within same half-page, but bit 8 is different. + data: "Fire \u0514\u0593" + }, { + encode: 0x82, + // 2 UCS2 character over 0x81 can encode range. + data: "Fire \u8000\u8001" + }, { + encode: 0x82, + // 2 UCS2 character over 0x81 can encode range. + data: "Fire \ufffd\ufffe" + }]; + + for (let i = 0; i < test_data.length; i++) { + let test = test_data[i]; + let writtenStr = iccHelper.writeICCUCS2String(alphaLen, test.data); + equal(writtenStr, test.data); + equal(helper.readHexOctet(), test.encode); + equal(iccHelper.readICCUCS2String(test.encode, alphaLen - 1), test.data); + } + + // This string use 0x80 encoded and the maximum capacity is 17 octets. + // Each alphabet takes 2 octets, thus the first 8 alphabets can be written. + let str = "Mozilla \u82b3\u8233 On Fire"; + let writtenStr = iccHelper.writeICCUCS2String(alphaLen, str); + equal(writtenStr, str.substring(0, 8)); + equal(helper.readHexOctet(), 0x80); + equal(iccHelper.readICCUCS2String(0x80, alphaLen - 1), str.substring(0, 8)); + + // This string use 0x81 encoded and the maximum capacity is 15 octets. + // Each alphabet takes 1 octets, thus the first 15 alphabets can be written. + str = "Mozilla \u6901\u697f On Fire"; + writtenStr = iccHelper.writeICCUCS2String(alphaLen, str); + equal(writtenStr, str.substring(0, 15)); + equal(helper.readHexOctet(), 0x81); + equal(iccHelper.readICCUCS2String(0x81, alphaLen - 1), str.substring(0, 15)); + + // This string use 0x82 encoded and the maximum capacity is 14 octets. + // Each alphabet takes 1 octets, thus the first 14 alphabets can be written. + str = "Mozilla \u0514\u0593 On Fire"; + writtenStr = iccHelper.writeICCUCS2String(alphaLen, str); + equal(writtenStr, str.substring(0, 14)); + equal(helper.readHexOctet(), 0x82); + equal(iccHelper.readICCUCS2String(0x82, alphaLen - 1), str.substring(0, 14)); + + run_next_test(); +}); +/** + * Verify ICCPDUHelper#readDiallingNumber + */ +add_test(function test_read_dialling_number() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + let str = "123456789"; + + helper.readHexOctet = function() { + return 0x81; + }; + + helper.readSwappedNibbleExtendedBcdString = function(len) { + return str.substring(0, len); + }; + + for (let i = 0; i < str.length; i++) { + equal(str.substring(0, i - 1), // -1 for the TON + iccHelper.readDiallingNumber(i)); + } + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper#read8BitUnpackedToString + */ +add_test(function test_read_8bit_unpacked_to_string() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + // Test 1: Read GSM alphabets. + // Write alphabets before ESCAPE. + for (let i = 0; i < PDU_NL_EXTENDED_ESCAPE; i++) { + helper.writeHexOctet(i); + } + + // Write two ESCAPEs to make it become ' '. + helper.writeHexOctet(PDU_NL_EXTENDED_ESCAPE); + helper.writeHexOctet(PDU_NL_EXTENDED_ESCAPE); + + for (let i = PDU_NL_EXTENDED_ESCAPE + 1; i < langTable.length; i++) { + helper.writeHexOctet(i); + } + + // Also write two unused fields. + let ffLen = 2; + for (let i = 0; i < ffLen; i++) { + helper.writeHexOctet(0xff); + } + + equal(iccHelper.read8BitUnpackedToString(PDU_NL_EXTENDED_ESCAPE), + langTable.substring(0, PDU_NL_EXTENDED_ESCAPE)); + equal(iccHelper.read8BitUnpackedToString(2), " "); + equal(iccHelper.read8BitUnpackedToString(langTable.length - + PDU_NL_EXTENDED_ESCAPE - 1 + ffLen), + langTable.substring(PDU_NL_EXTENDED_ESCAPE + 1)); + + // Test 2: Read GSM extended alphabets. + for (let i = 0; i < langShiftTable.length; i++) { + helper.writeHexOctet(PDU_NL_EXTENDED_ESCAPE); + helper.writeHexOctet(i); + } + + // Read string before RESERVED_CONTROL. + equal(iccHelper.read8BitUnpackedToString(PDU_NL_RESERVED_CONTROL * 2), + langShiftTable.substring(0, PDU_NL_RESERVED_CONTROL)); + // ESCAPE + RESERVED_CONTROL will become ' '. + equal(iccHelper.read8BitUnpackedToString(2), " "); + // Read string between RESERVED_CONTROL and EXTENDED_ESCAPE. + equal(iccHelper.read8BitUnpackedToString( + (PDU_NL_EXTENDED_ESCAPE - PDU_NL_RESERVED_CONTROL - 1) * 2), + langShiftTable.substring(PDU_NL_RESERVED_CONTROL + 1, + PDU_NL_EXTENDED_ESCAPE)); + // ESCAPE + ESCAPE will become ' '. + equal(iccHelper.read8BitUnpackedToString(2), " "); + // Read remaining string. + equal(iccHelper.read8BitUnpackedToString( + (langShiftTable.length - PDU_NL_EXTENDED_ESCAPE - 1) * 2), + langShiftTable.substring(PDU_NL_EXTENDED_ESCAPE + 1)); + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper#writeStringTo8BitUnpacked. + * + * Test writing GSM 8 bit alphabets. + */ +add_test(function test_write_string_to_8bit_unpacked() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + // Length of trailing 0xff. + let ffLen = 2; + let str; + + // Test 1, write GSM alphabets. + let writtenStr = iccHelper.writeStringTo8BitUnpacked(langTable.length + ffLen, langTable); + equal(writtenStr, langTable); + + for (let i = 0; i < langTable.length; i++) { + equal(helper.readHexOctet(), i); + } + + for (let i = 0; i < ffLen; i++) { + equal(helper.readHexOctet(), 0xff); + } + + // Test 2, write GSM extended alphabets. + str = "\u000c\u20ac"; + writtenStr = iccHelper.writeStringTo8BitUnpacked(4, str); + equal(writtenStr, str); + equal(iccHelper.read8BitUnpackedToString(4), str); + + // Test 3, write GSM and GSM extended alphabets. + // \u000c, \u20ac are from gsm extended alphabets. + // \u00a3 is from gsm alphabet. + str = "\u000c\u20ac\u00a3"; + + // 2 octets * 2 = 4 octets for 2 gsm extended alphabets, + // 1 octet for 1 gsm alphabet, + // 2 octes for trailing 0xff. + // "Totally 7 octets are to be written." + writtenStr = iccHelper.writeStringTo8BitUnpacked(7, str); + equal(writtenStr, str); + equal(iccHelper.read8BitUnpackedToString(7), str); + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper#writeStringTo8BitUnpacked with maximum octets written. + */ +add_test(function test_write_string_to_8bit_unpacked_with_max_octets_written() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + // The maximum of the number of octets that can be written is 3. + // Only 3 characters shall be written even the length of the string is 4. + let writtenStr = iccHelper.writeStringTo8BitUnpacked(3, langTable.substring(0, 4)); + equal(writtenStr, langTable.substring(0, 3)); + helper.writeHexOctet(0xff); // dummy octet. + for (let i = 0; i < 3; i++) { + equal(helper.readHexOctet(), i); + } + ok(helper.readHexOctet() != 4); + + // \u000c is GSM extended alphabet, 2 octets. + // \u00a3 is GSM alphabet, 1 octet. + let str = "\u000c\u00a3"; + writtenStr = iccHelper.writeStringTo8BitUnpacked(3, str); + equal(writtenStr, str.substring(0, 2)); + equal(iccHelper.read8BitUnpackedToString(3), str); + + str = "\u00a3\u000c"; + writtenStr = iccHelper.writeStringTo8BitUnpacked(3, str); + equal(writtenStr, str.substring(0, 2)); + equal(iccHelper.read8BitUnpackedToString(3), str); + + // 2 GSM extended alphabets cost 4 octets, but maximum is 3, so only the 1st + // alphabet can be written. + str = "\u000c\u000c"; + writtenStr = iccHelper.writeStringTo8BitUnpacked(3, str); + helper.writeHexOctet(0xff); // dummy octet. + equal(writtenStr, str.substring(0, 1)); + equal(iccHelper.read8BitUnpackedToString(4), str.substring(0, 1)); + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper.readAlphaIdentifier + */ +add_test(function test_read_alpha_identifier() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + + // UCS2: 0x80 + let text = "TEST"; + helper.writeHexOctet(0x80); + helper.writeUCS2String(text); + // Also write two unused octets. + let ffLen = 2; + for (let i = 0; i < ffLen; i++) { + helper.writeHexOctet(0xff); + } + equal(iccHelper.readAlphaIdentifier(1 + (2 * text.length) + ffLen), text); + + // UCS2: 0x81 + let array = [0x81, 0x08, 0xd2, 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, 0xca, 0xff, 0xff]; + for (let i = 0; i < array.length; i++) { + helper.writeHexOctet(array[i]); + } + equal(iccHelper.readAlphaIdentifier(array.length), "Mozilla\u694a"); + + // UCS2: 0x82 + let array2 = [0x82, 0x08, 0x69, 0x00, 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, 0xca, 0xff, 0xff]; + for (let i = 0; i < array2.length; i++) { + helper.writeHexOctet(array2[i]); + } + equal(iccHelper.readAlphaIdentifier(array2.length), "Mozilla\u694a"); + + // GSM 8 Bit Unpacked + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + for (let i = 0; i < PDU_NL_EXTENDED_ESCAPE; i++) { + helper.writeHexOctet(i); + } + equal(iccHelper.readAlphaIdentifier(PDU_NL_EXTENDED_ESCAPE), + langTable.substring(0, PDU_NL_EXTENDED_ESCAPE)); + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper.writeAlphaIdentifier + */ +add_test(function test_write_alpha_identifier() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + // Length of trailing 0xff. + let ffLen = 2; + + // Removal + let writenAlphaId = iccHelper.writeAlphaIdentifier(10, null); + equal(writenAlphaId, ""); + equal(iccHelper.readAlphaIdentifier(10), ""); + + // GSM 8 bit + let str = "Mozilla"; + writenAlphaId = iccHelper.writeAlphaIdentifier(str.length + ffLen, str); + equal(writenAlphaId , str); + equal(iccHelper.readAlphaIdentifier(str.length + ffLen), str); + + // UCS2 + str = "Mozilla\u8000"; + writenAlphaId = iccHelper.writeAlphaIdentifier(str.length * 2 + ffLen, str); + equal(writenAlphaId , str); + // * 2 for each character will be encoded to UCS2 alphabets. + equal(iccHelper.readAlphaIdentifier(str.length * 2 + ffLen), str); + + // Test with maximum octets written. + // 1 coding scheme (0x80) and 1 UCS2 character, total 3 octets. + str = "\u694a"; + writenAlphaId = iccHelper.writeAlphaIdentifier(3, str); + equal(writenAlphaId , str); + equal(iccHelper.readAlphaIdentifier(3), str); + + // 1 coding scheme (0x80) and 2 UCS2 characters, total 5 octets. + // numOctets is limited to 4, so only 1 UCS2 character can be written. + str = "\u694a\u69ca"; + writenAlphaId = iccHelper.writeAlphaIdentifier(4, str); + helper.writeHexOctet(0xff); // dummy octet. + equal(writenAlphaId , str.substring(0, 1)); + equal(iccHelper.readAlphaIdentifier(5), str.substring(0, 1)); + + // Write 0 octet. + writenAlphaId = iccHelper.writeAlphaIdentifier(0, "1"); + helper.writeHexOctet(0xff); // dummy octet. + equal(writenAlphaId, ""); + equal(iccHelper.readAlphaIdentifier(1), ""); + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper.readAlphaIdDiallingNumber + */ +add_test(function test_read_alpha_id_dialling_number() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + let buf = context.Buf; + const recordSize = 32; + + function testReadAlphaIdDiallingNumber(contact) { + iccHelper.readAlphaIdentifier = function() { + return contact.alphaId; + }; + + iccHelper.readNumberWithLength = function() { + return contact.number; + }; + + let strLen = recordSize * 2; + buf.writeInt32(strLen); // fake length + helper.writeHexOctet(0xff); // fake CCP + helper.writeHexOctet(0xff); // fake EXT1 + buf.writeStringDelimiter(strLen); + + let contactR = iccHelper.readAlphaIdDiallingNumber(recordSize); + if (contact.alphaId == "" && contact.number == "") { + equal(contactR, null); + } else { + equal(contactR.alphaId, contact.alphaId); + equal(contactR.number, contact.number); + } + } + + testReadAlphaIdDiallingNumber({alphaId: "AlphaId", number: "0987654321"}); + testReadAlphaIdDiallingNumber({alphaId: "", number: ""}); + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper.writeAlphaIdDiallingNumber + */ +add_test(function test_write_alpha_id_dialling_number() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.ICCPDUHelper; + const recordSize = 32; + + // Write a normal contact. + let contactW = { + alphaId: "Mozilla", + number: "1234567890" + }; + + let writtenContact = helper.writeAlphaIdDiallingNumber(recordSize, + contactW.alphaId, + contactW.number, 0xff); + + let contactR = helper.readAlphaIdDiallingNumber(recordSize); + equal(writtenContact.alphaId, contactR.alphaId); + equal(writtenContact.number, contactR.number); + equal(0xff, contactR.extRecordNumber); + + // Write a contact with alphaId encoded in UCS2 and number has '+'. + let contactUCS2 = { + alphaId: "火狐", + number: "+1234567890" + }; + + writtenContact = helper.writeAlphaIdDiallingNumber(recordSize, + contactUCS2.alphaId, + contactUCS2.number, 0xff); + contactR = helper.readAlphaIdDiallingNumber(recordSize); + equal(writtenContact.alphaId, contactR.alphaId); + equal(writtenContact.number, contactR.number); + equal(0xff, contactR.extRecordNumber); + + // Write a null contact (Removal). + writtenContact = helper.writeAlphaIdDiallingNumber(recordSize); + contactR = helper.readAlphaIdDiallingNumber(recordSize); + equal(contactR, null); + equal(writtenContact.alphaId, ""); + equal(writtenContact.number, ""); + + // Write a longer alphaId/dialling number + // Dialling Number : Maximum 20 digits(10 octets). + // Alpha Identifier: 32(recordSize) - 14 (10 octets for Dialling Number, 1 + // octet for TON/NPI, 1 for number length octet, and 2 for + // Ext) = Maximum 18 octets. + let longContact = { + alphaId: "AAAAAAAAABBBBBBBBBCCCCCCCCC", + number: "123456789012345678901234567890", + }; + + writtenContact = helper.writeAlphaIdDiallingNumber(recordSize, + longContact.alphaId, + longContact.number, 0xff); + contactR = helper.readAlphaIdDiallingNumber(recordSize); + equal(writtenContact.alphaId, contactR.alphaId); + equal(writtenContact.number, contactR.number); + equal(0xff, contactR.extRecordNumber); + + // Add '+' to number and test again. + longContact.number = "+123456789012345678901234567890"; + writtenContact = helper.writeAlphaIdDiallingNumber(recordSize, + longContact.alphaId, + longContact.number, 0xff); + contactR = helper.readAlphaIdDiallingNumber(recordSize); + equal(writtenContact.alphaId, contactR.alphaId); + equal(writtenContact.number, contactR.number); + equal(0xff, contactR.extRecordNumber); + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper.writeDiallingNumber + */ +add_test(function test_write_dialling_number() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.ICCPDUHelper; + + // with + + let number = "+123456"; + let len = 4; + helper.writeDiallingNumber(number); + equal(helper.readDiallingNumber(len), number); + + // without + + number = "987654"; + len = 4; + helper.writeDiallingNumber(number); + equal(helper.readDiallingNumber(len), number); + + number = "9876543"; + len = 5; + helper.writeDiallingNumber(number); + equal(helper.readDiallingNumber(len), number); + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper.readNumberWithLength + */ +add_test(function test_read_number_with_length() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + + let numbers = [ + { + number: "123456789", + expectedNumber: "123456789" + }, + { + number: "", + expectedNumber: "" + }, + // Invalid length of BCD number/SSC contents + { + number: "12345678901234567890123", + expectedNumber: "" + }, + ]; + + // To avoid obtaining wrong buffer content. + context.Buf.seekIncoming = function(offset) { + }; + + function do_test(aNumber, aExpectedNumber) { + iccHelper.readDiallingNumber = function(numLen) { + return aNumber.substring(0, numLen); + }; + + if (aNumber) { + helper.writeHexOctet(aNumber.length + 1); + } else { + helper.writeHexOctet(0xff); + } + + equal(iccHelper.readNumberWithLength(), aExpectedNumber); + } + + for (let i = 0; i < numbers.length; i++) { + do_test(numbers[i].number, numbers[i].expectedNumber); + } + + run_next_test(); +}); + +/** + * Verify ICCPDUHelper.writeNumberWithLength + */ +add_test(function test_write_number_with_length() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + + function test(number, expectedNumber) { + expectedNumber = expectedNumber || number; + let writeNumber = iccHelper.writeNumberWithLength(number); + equal(writeNumber, expectedNumber); + let numLen = helper.readHexOctet(); + equal(expectedNumber, iccHelper.readDiallingNumber(numLen)); + for (let i = 0; i < (ADN_MAX_BCD_NUMBER_BYTES - numLen); i++) { + equal(0xff, helper.readHexOctet()); + } + } + + // without + + test("123456789"); + + // with + + test("+987654321"); + + // extended BCD coding + test("1*2#3,4*5#6,"); + + // with + and extended BCD coding + test("+1*2#3,4*5#6,"); + + // non-supported characters should not be written. + test("(1)23-456+789", "123456789"); + + test("++(01)2*3-4#5,6+7(8)9*0#1,", "+012*34#5,6789*0#1,"); + + // over maximum 20 digits should be truncated. + test("012345678901234567890123456789", "01234567890123456789"); + + // null + iccHelper.writeNumberWithLength(null); + for (let i = 0; i < (ADN_MAX_BCD_NUMBER_BYTES + 1); i++) { + equal(0xff, helper.readHexOctet()); + } + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_icc_ICCRecordHelper.js b/dom/system/gonk/tests/test_ril_worker_icc_ICCRecordHelper.js new file mode 100644 index 000000000..00c55873d --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_ICCRecordHelper.js @@ -0,0 +1,1080 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify ICCRecordHelper.readPBR + */ +add_test(function test_read_pbr() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let record = context.ICCRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + let pbr_1 = [ + 0xa8, 0x05, 0xc0, 0x03, 0x4f, 0x3a, 0x01 + ]; + + // Write data size + buf.writeInt32(pbr_1.length * 2); + + // Write pbr + for (let i = 0; i < pbr_1.length; i++) { + helper.writeHexOctet(pbr_1[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(pbr_1.length * 2); + + options.totalRecords = 2; + if (options.callback) { + options.callback(options); + } + }; + + io.loadNextRecord = function fakeLoadNextRecord(options) { + let pbr_2 = [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff + ]; + + options.p1++; + if (options.callback) { + options.callback(options); + } + }; + + let successCb = function successCb(pbrs) { + equal(pbrs[0].adn.fileId, 0x4f3a); + equal(pbrs.length, 1); + }; + + let errorCb = function errorCb(errorMsg) { + do_print("Reading EF_PBR failed, msg = " + errorMsg); + ok(false); + }; + + record.readPBR(successCb, errorCb); + + // Check cache pbrs when 2nd call + let ifLoadEF = false; + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + ifLoadEF = true; + } + record.readPBR(successCb, errorCb); + ok(!ifLoadEF); + + run_next_test(); +}); + +/** + * Verify ICCRecordHelper.readEmail + */ +add_test(function test_read_email() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let record = context.ICCRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + let recordSize; + + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + let email_1 = [ + 0x65, 0x6D, 0x61, 0x69, 0x6C, + 0x00, 0x6D, 0x6F, 0x7A, 0x69, + 0x6C, 0x6C, 0x61, 0x2E, 0x63, + 0x6F, 0x6D, 0x02, 0x23]; + + // Write data size + buf.writeInt32(email_1.length * 2); + + // Write email + for (let i = 0; i < email_1.length; i++) { + helper.writeHexOctet(email_1[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(email_1.length * 2); + + recordSize = email_1.length; + options.recordSize = recordSize; + if (options.callback) { + options.callback(options); + } + }; + + function doTestReadEmail(type, expectedResult) { + let fileId = 0x6a75; + let recordNumber = 1; + + // fileId and recordNumber are dummy arguments. + record.readEmail(fileId, type, recordNumber, function(email) { + equal(email, expectedResult); + }); + }; + + doTestReadEmail(ICC_USIM_TYPE1_TAG, "email@mozilla.com$#"); + doTestReadEmail(ICC_USIM_TYPE2_TAG, "email@mozilla.com"); + equal(record._emailRecordSize, recordSize); + + run_next_test(); +}); + +/** + * Verify ICCRecordHelper.updateEmail + */ +add_test(function test_update_email() { + const recordSize = 0x20; + const recordNumber = 1; + const fileId = 0x4f50; + const NUM_TESTS = 2; + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + let ril = context.RIL; + ril.appType = CARD_APPTYPE_USIM; + let recordHelper = context.ICCRecordHelper; + let buf = context.Buf; + let ioHelper = context.ICCIOHelper; + let pbr = {email: {fileId: fileId, fileType: ICC_USIM_TYPE1_TAG}, + adn: {sfi: 1}}; + let count = 0; + + // Override. + ioHelper.updateLinearFixedEF = function(options) { + options.pathId = context.ICCFileHelper.getEFPath(options.fileId); + options.command = ICC_COMMAND_UPDATE_RECORD; + options.p1 = options.recordNumber; + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = recordSize; + ril.iccIO(options); + }; + + function do_test(pbr, expectedEmail, expectedAdnRecordId) { + buf.sendParcel = function() { + count++; + + // Request Type. + equal(this.readInt32(), REQUEST_SIM_IO); + + // Token : we don't care + this.readInt32(); + + // command. + equal(this.readInt32(), ICC_COMMAND_UPDATE_RECORD); + + // fileId. + equal(this.readInt32(), fileId); + + // pathId. + equal(this.readString(), + EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_DF_PHONEBOOK); + + // p1. + equal(this.readInt32(), recordNumber); + + // p2. + equal(this.readInt32(), READ_RECORD_ABSOLUTE_MODE); + + // p3. + equal(this.readInt32(), recordSize); + + // data. + let strLen = this.readInt32(); + let email; + if (pbr.email.fileType === ICC_USIM_TYPE1_TAG) { + email = iccHelper.read8BitUnpackedToString(recordSize); + } else { + email = iccHelper.read8BitUnpackedToString(recordSize - 2); + equal(pduHelper.readHexOctet(), pbr.adn.sfi); + equal(pduHelper.readHexOctet(), expectedAdnRecordId); + } + this.readStringDelimiter(strLen); + equal(email, expectedEmail); + + // pin2. + equal(this.readString(), null); + + // AID. Ignore because it's from modem. + this.readInt32(); + + if (count == NUM_TESTS) { + run_next_test(); + } + }; + recordHelper.updateEmail(pbr, recordNumber, expectedEmail, expectedAdnRecordId); + } + + do_test(pbr, "test@mail.com"); + pbr.email.fileType = ICC_USIM_TYPE2_TAG; + do_test(pbr, "test@mail.com", 1); +}); + +/** + * Verify ICCRecordHelper.readANR + */ +add_test(function test_read_anr() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let record = context.ICCRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + let recordSize; + + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + let anr_1 = [ + 0x01, 0x05, 0x81, 0x10, 0x32, + 0x54, 0xF6, 0xFF, 0xFF]; + + // Write data size + buf.writeInt32(anr_1.length * 2); + + // Write anr + for (let i = 0; i < anr_1.length; i++) { + helper.writeHexOctet(anr_1[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(anr_1.length * 2); + + recordSize = anr_1.length; + options.recordSize = recordSize; + if (options.callback) { + options.callback(options); + } + }; + + function doTestReadAnr(fileType, expectedResult) { + let fileId = 0x4f11; + let recordNumber = 1; + + // fileId and recordNumber are dummy arguments. + record.readANR(fileId, fileType, recordNumber, function(anr) { + equal(anr, expectedResult); + }); + }; + + doTestReadAnr(ICC_USIM_TYPE1_TAG, "0123456"); + equal(record._anrRecordSize, recordSize); + + run_next_test(); +}); + +/** + * Verify ICCRecordHelper.updateANR + */ +add_test(function test_update_anr() { + const recordSize = 0x20; + const recordNumber = 1; + const fileId = 0x4f11; + const NUM_TESTS = 2; + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + let ril = context.RIL; + ril.appType = CARD_APPTYPE_USIM; + let recordHelper = context.ICCRecordHelper; + let buf = context.Buf; + let ioHelper = context.ICCIOHelper; + let pbr = {anr0: {fileId: fileId, fileType: ICC_USIM_TYPE1_TAG}, + adn: {sfi: 1}}; + let count = 0; + + // Override. + ioHelper.updateLinearFixedEF = function(options) { + options.pathId = context.ICCFileHelper.getEFPath(options.fileId); + options.command = ICC_COMMAND_UPDATE_RECORD; + options.p1 = options.recordNumber; + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = recordSize; + ril.iccIO(options); + }; + + function do_test(pbr, expectedANR, expectedAdnRecordId) { + buf.sendParcel = function() { + count++; + + // Request Type. + equal(this.readInt32(), REQUEST_SIM_IO); + + // Token : we don't care + this.readInt32(); + + // command. + equal(this.readInt32(), ICC_COMMAND_UPDATE_RECORD); + + // fileId. + equal(this.readInt32(), fileId); + + // pathId. + equal(this.readString(), + EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_DF_PHONEBOOK); + + // p1. + equal(this.readInt32(), recordNumber); + + // p2. + equal(this.readInt32(), READ_RECORD_ABSOLUTE_MODE); + + // p3. + equal(this.readInt32(), recordSize); + + // data. + let strLen = this.readInt32(); + // EF_AAS, ignore. + pduHelper.readHexOctet(); + equal(iccHelper.readNumberWithLength(), expectedANR); + // EF_CCP, ignore. + pduHelper.readHexOctet(); + // EF_EXT1, ignore. + pduHelper.readHexOctet(); + if (pbr.anr0.fileType === ICC_USIM_TYPE2_TAG) { + equal(pduHelper.readHexOctet(), pbr.adn.sfi); + equal(pduHelper.readHexOctet(), expectedAdnRecordId); + } + this.readStringDelimiter(strLen); + + // pin2. + equal(this.readString(), null); + + // AID. Ignore because it's from modem. + this.readInt32(); + + if (count == NUM_TESTS) { + run_next_test(); + } + }; + recordHelper.updateANR(pbr, recordNumber, expectedANR, expectedAdnRecordId); + } + + do_test(pbr, "+123456789"); + pbr.anr0.fileType = ICC_USIM_TYPE2_TAG; + do_test(pbr, "123456789", 1); +}); + +/** + * Verify ICCRecordHelper.readIAP + */ +add_test(function test_read_iap() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let record = context.ICCRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + let recordSize; + + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + let iap_1 = [0x01, 0x02]; + + // Write data size/ + buf.writeInt32(iap_1.length * 2); + + // Write iap. + for (let i = 0; i < iap_1.length; i++) { + helper.writeHexOctet(iap_1[i]); + } + + // Write string delimiter. + buf.writeStringDelimiter(iap_1.length * 2); + + recordSize = iap_1.length; + options.recordSize = recordSize; + if (options.callback) { + options.callback(options); + } + }; + + function doTestReadIAP(expectedIAP) { + const fileId = 0x4f17; + const recordNumber = 1; + + let successCb = function successCb(iap) { + for (let i = 0; i < iap.length; i++) { + equal(expectedIAP[i], iap[i]); + } + run_next_test(); + }.bind(this); + + let errorCb = function errorCb(errorMsg) { + do_print(errorMsg); + ok(false); + run_next_test(); + }.bind(this); + + record.readIAP(fileId, recordNumber, successCb, errorCb); + }; + + doTestReadIAP([1, 2]); +}); + +/** + * Verify ICCRecordHelper.updateIAP + */ +add_test(function test_update_iap() { + const recordSize = 2; + const recordNumber = 1; + const fileId = 0x4f17; + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let ril = context.RIL; + ril.appType = CARD_APPTYPE_USIM; + let recordHelper = context.ICCRecordHelper; + let buf = context.Buf; + let ioHelper = context.ICCIOHelper; + let count = 0; + + // Override. + ioHelper.updateLinearFixedEF = function(options) { + options.pathId = context.ICCFileHelper.getEFPath(options.fileId); + options.command = ICC_COMMAND_UPDATE_RECORD; + options.p1 = options.recordNumber; + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = recordSize; + ril.iccIO(options); + }; + + function do_test(expectedIAP) { + buf.sendParcel = function() { + // Request Type. + equal(this.readInt32(), REQUEST_SIM_IO); + + // Token : we don't care + this.readInt32(); + + // command. + equal(this.readInt32(), ICC_COMMAND_UPDATE_RECORD); + + // fileId. + equal(this.readInt32(), fileId); + + // pathId. + equal(this.readString(), + EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_DF_PHONEBOOK); + + // p1. + equal(this.readInt32(), recordNumber); + + // p2. + equal(this.readInt32(), READ_RECORD_ABSOLUTE_MODE); + + // p3. + equal(this.readInt32(), recordSize); + + // data. + let strLen = this.readInt32(); + for (let i = 0; i < recordSize; i++) { + equal(expectedIAP[i], pduHelper.readHexOctet()); + } + this.readStringDelimiter(strLen); + + // pin2. + equal(this.readString(), null); + + // AID. Ignore because it's from modem. + this.readInt32(); + + run_next_test(); + }; + recordHelper.updateIAP(fileId, recordNumber, expectedIAP); + } + + do_test([1, 2]); +}); + +/** + * Verify ICCRecordHelper.readADNLike. + */ +add_test(function test_read_adn_like() { + const RECORD_SIZE = 0x20; + + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let record = context.ICCRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + let ril = context.RIL; + + function do_test(extFileId, rawEF, expectedExtRecordNumber, expectedNumber) { + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size + buf.writeInt32(rawEF.length * 2); + + // Write adn + for (let i = 0; i < rawEF.length; i += 2) { + helper.writeHexOctet(parseInt(rawEF.substr(i, 2), 16)); + } + + // Write string delimiter + buf.writeStringDelimiter(rawEF.length * 2); + + options.p1 = 1; + options.recordSize = RECORD_SIZE; + options.totalRecords = 1; + if (options.callback) { + options.callback(options); + } + }; + + record.readExtension = function(fileId, recordNumber, onsuccess, onerror) { + onsuccess("1234"); + } + + let successCb = function successCb(contacts) { + ok(contacts[0].number == expectedNumber); + }; + + let errorCb = function errorCb(errorMsg) { + do_print("Reading ADNLike failed, msg = " + errorMsg); + ok(false); + }; + + record.readADNLike(ICC_EF_ADN, extFileId, successCb, errorCb); + } + + ril.appType = CARD_APPTYPE_SIM; + // Valid extension + do_test(ICC_EF_EXT1,"436f6e74616374303031ffffffffffffffff0b8199887766554433221100ff01", + 0x01,"998877665544332211001234"); + // Empty extension + do_test(ICC_EF_EXT1,"436f6e74616374303031ffffffffffffffff0b8199887766554433221100ffff", + 0xff, "99887766554433221100"); + // Unsupport extension + do_test(null,"436f6e74616374303031ffffffffffffffff0b8199887766554433221100ffff", + 0xff, "99887766554433221100"); + // Empty dialling number contact + do_test(null,"436f6e74616374303031ffffffffffffffffffffffffffffffffffffffffffff", + 0xff, ""); + + run_next_test(); +}); + +/** + * Verify ICCRecordHelper.updateADNLike. + */ +add_test(function test_update_adn_like() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let record = context.ICCRecordHelper; + let io = context.ICCIOHelper; + let pdu = context.ICCPDUHelper; + let buf = context.Buf; + + ril.appType = CARD_APPTYPE_SIM; + const recordSize = 0x20; + let fileId; + + // Override. + io.updateLinearFixedEF = function(options) { + options.pathId = context.ICCFileHelper.getEFPath(options.fileId); + options.command = ICC_COMMAND_UPDATE_RECORD; + options.p1 = options.recordNumber; + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = recordSize; + ril.iccIO(options); + }; + + buf.sendParcel = function() { + // Request Type. + equal(this.readInt32(), REQUEST_SIM_IO); + + // Token : we don't care + this.readInt32(); + + // command. + equal(this.readInt32(), ICC_COMMAND_UPDATE_RECORD); + + // fileId. + equal(this.readInt32(), fileId); + + // pathId. + equal(this.readString(), EF_PATH_MF_SIM + EF_PATH_DF_TELECOM); + + // p1. + equal(this.readInt32(), 1); + + // p2. + equal(this.readInt32(), READ_RECORD_ABSOLUTE_MODE); + + // p3. + equal(this.readInt32(), 0x20); + + // data. + let contact = pdu.readAlphaIdDiallingNumber(0x20); + equal(contact.alphaId, "test"); + equal(contact.number, "123456"); + equal(contact.extRecordNumber, "0xff"); + + // pin2. + if (fileId == ICC_EF_ADN) { + equal(this.readString(), null); + } else { + equal(this.readString(), "1111"); + } + + // AID. Ignore because it's from modem. + this.readInt32(); + + if (fileId == ICC_EF_FDN) { + run_next_test(); + } + }; + + fileId = ICC_EF_ADN; + record.updateADNLike(fileId, 0xff, + {recordId: 1, alphaId: "test", number: "123456"}); + + fileId = ICC_EF_FDN; + record.updateADNLike(fileId, 0xff, + {recordId: 1, alphaId: "test", number: "123456"}, + "1111"); +}); + +/** + * Verify ICCRecordHelper.findFreeRecordId. + */ +add_test(function test_find_free_record_id() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let recordHelper = context.ICCRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + let ril = context.RIL; + + function writeRecord(record) { + // Write data size + buf.writeInt32(record.length * 2); + + for (let i = 0; i < record.length; i++) { + pduHelper.writeHexOctet(record[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(record.length * 2); + } + + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Some random data. + let record = [0x12, 0x34, 0x56, 0x78, 0x90]; + options.p1 = 1; + options.totalRecords = 2; + writeRecord(record); + if (options.callback) { + options.callback(options); + } + }; + + ril.iccIO = function fakeIccIO(options) { + // Unused bytes. + let record = [0xff, 0xff, 0xff, 0xff, 0xff]; + writeRecord(record); + if (options.callback) { + options.callback(options); + } + }; + + let fileId = 0x0000; // Dummy. + recordHelper.findFreeRecordId( + fileId, + function(recordId) { + equal(recordId, 2); + run_next_test(); + }.bind(this), + function(errorMsg) { + do_print(errorMsg); + ok(false); + run_next_test(); + }.bind(this)); +}); + +/** + * Verify ICCRecordHelper.fetchICCRecords. + */ +add_test(function test_fetch_icc_recodes() { + let worker = newWorker(); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let iccRecord = context.ICCRecordHelper; + let simRecord = context.SimRecordHelper; + let ruimRecord = context.RuimRecordHelper; + let fetchTag = 0x00; + + simRecord.fetchSimRecords = function() { + fetchTag = 0x01; + }; + + ruimRecord.fetchRuimRecords = function() { + fetchTag = 0x02; + }; + + RIL.appType = CARD_APPTYPE_SIM; + iccRecord.fetchICCRecords(); + equal(fetchTag, 0x01); + + RIL.appType = CARD_APPTYPE_RUIM; + iccRecord.fetchICCRecords(); + equal(fetchTag, 0x02); + + RIL.appType = CARD_APPTYPE_USIM; + iccRecord.fetchICCRecords(); + equal(fetchTag, 0x01); + + run_next_test(); +}); + +/** + * Verify reading EF_ICCID. + */ +add_test(function test_handling_iccid() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.ICCRecordHelper; + let helper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + ril.reportStkServiceIsRunning = function fakeReportStkServiceIsRunning() { + }; + + function do_test(rawICCID, expectedICCID) { + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // Write data size + buf.writeInt32(rawICCID.length); + + // Write data + for (let i = 0; i < rawICCID.length; i += 2) { + helper.writeHexOctet(parseInt(rawICCID.substr(i, 2), 16)); + } + + // Write string delimiter + buf.writeStringDelimiter(rawICCID.length); + + if (options.callback) { + options.callback(options); + } + }; + + record.readICCID(); + + equal(ril.iccInfo.iccid, expectedICCID); + } + + // Invalid value 0xE at high nibbile + low nibbile contains 0xF. + do_test("9868002E90909F001519", "89860020909"); + // Invalid value 0xD at low nibbile. + do_test("986800D2909090001519", "8986002090909005191"); + // Invalid value 0xC at low nibbile. + do_test("986800C2909090001519", "8986002090909005191"); + // Invalid value 0xB at low nibbile. + do_test("986800B2909090001519", "8986002090909005191"); + // Invalid value 0xA at low nibbile. + do_test("986800A2909090001519", "8986002090909005191"); + // Valid ICCID. + do_test("98101430121181157002", "89014103211118510720"); + + run_next_test(); +}); + +/** + * Verify ICCRecordHelper.readExtension + */ +add_test(function test_read_extension() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let record = context.ICCRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + + function do_test(rawExtension, expectedExtensionNumber) { + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size + buf.writeInt32(rawExtension.length * 2); + + // Write ext + for (let i = 0; i < rawExtension.length; i += 2) { + helper.writeHexOctet(parseInt(rawExtension.substr(i, 2), 16)); + } + + // Write string delimiter + buf.writeStringDelimiter(rawExtension.length); + + if (options.callback) { + options.callback(options); + } + }; + + let successCb = function successCb(number) { + do_print("extension number:" + number); + equal(number, expectedExtensionNumber); + }; + + let errorCb = function errorCb() { + ok(expectedExtensionNumber == null); + }; + + record.readExtension(0x6f4a, 1, successCb, errorCb); + } + + // Test unsupported record type 0x01 + do_test("010a10325476981032547698ff", ""); + // Test invalid length 0xc1 + do_test("020c10325476981032547698ff", null); + // Test extension chain which we don't support + do_test("020a1032547698103254769802", "01234567890123456789"); + // Test valid Extension + do_test("020a10325476981032547698ff", "01234567890123456789"); + // Test valid Extension + do_test("0209103254769810325476ffff", "012345678901234567"); + // Test empty Extension + do_test("02ffffffffffffffffffffffff", ""); + + run_next_test(); +}); + +/** + * Verify ICCRecordHelper.updateExtension + */ +add_test(function test_update_extension() { + const RECORD_SIZE = 13; + const RECORD_NUMBER = 1; + + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let ril = context.RIL; + let recordHelper = context.ICCRecordHelper; + let buf = context.Buf; + let ioHelper = context.ICCIOHelper; + + // Override. + ioHelper.updateLinearFixedEF = function(options) { + options.pathId = context.ICCFileHelper.getEFPath(options.fileId); + options.command = ICC_COMMAND_UPDATE_RECORD; + options.p1 = options.recordNumber; + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = RECORD_SIZE; + ril.iccIO(options); + }; + + function do_test(fileId, number, expectedNumber) { + buf.sendParcel = function() { + // Request Type. + equal(this.readInt32(), REQUEST_SIM_IO); + + // Token : we don't care + this.readInt32(); + + // command. + equal(this.readInt32(), ICC_COMMAND_UPDATE_RECORD); + + // fileId. + equal(this.readInt32(), fileId); + + // pathId. + if (ril.appType == CARD_APPTYPE_SIM || ril.appType == CARD_APPTYPE_RUIM) { + equal(this.readString(), + EF_PATH_MF_SIM + EF_PATH_DF_TELECOM); + } else{ + equal(this.readString(), + EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_DF_PHONEBOOK); + } + + // p1. + equal(this.readInt32(), RECORD_NUMBER); + + // p2. + equal(this.readInt32(), READ_RECORD_ABSOLUTE_MODE); + + // p3. + equal(this.readInt32(), RECORD_SIZE); + + // data. + let strLen = this.readInt32(); + // Extension record + let recordType = pduHelper.readHexOctet(); + + equal(recordType, 0x02); + equal(pduHelper.readHexOctet(), 10); + equal( + pduHelper.readSwappedNibbleExtendedBcdString(EXT_MAX_NUMBER_DIGITS - 1), + expectedNumber); + + this.readStringDelimiter(strLen); + + // pin2. + equal(this.readString(), null); + + // AID. Ignore because it's from modem. + this.readInt32(); + }; + + recordHelper.updateExtension(fileId, RECORD_NUMBER, number); + } + + ril.appType = CARD_APPTYPE_SIM; + do_test(ICC_EF_EXT1, "01234567890123456789", "01234567890123456789"); + // We don't support extension chain. + do_test(ICC_EF_EXT1, "012345678901234567891234", "01234567890123456789"); + + ril.appType = CARD_APPTYPE_USIM; + do_test(ICC_EF_EXT1, "01234567890123456789", "01234567890123456789"); + + ril.appType = CARD_APPTYPE_RUIM; + do_test(ICC_EF_EXT1, "01234567890123456789", "01234567890123456789"); + + run_next_test(); +}); + +/** + * Verify ICCRecordHelper.cleanEFRecord + */ +add_test(function test_clean_ef_record() { + const RECORD_SIZE = 13; + const RECORD_NUMBER = 1; + + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let ril = context.RIL; + let recordHelper = context.ICCRecordHelper; + let buf = context.Buf; + let ioHelper = context.ICCIOHelper; + + // Override. + ioHelper.updateLinearFixedEF = function(options) { + options.pathId = context.ICCFileHelper.getEFPath(options.fileId); + options.command = ICC_COMMAND_UPDATE_RECORD; + options.p1 = options.recordNumber; + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = RECORD_SIZE; + ril.iccIO(options); + }; + + function do_test(fileId) { + buf.sendParcel = function() { + // Request Type. + equal(this.readInt32(), REQUEST_SIM_IO); + + // Token : we don't care + this.readInt32(); + + // command. + equal(this.readInt32(), ICC_COMMAND_UPDATE_RECORD); + + // fileId. + equal(this.readInt32(), fileId); + + // pathId. + if (ril.appType == CARD_APPTYPE_SIM || ril.appType == CARD_APPTYPE_RUIM) { + equal(this.readString(), + EF_PATH_MF_SIM + EF_PATH_DF_TELECOM); + } else{ + equal(this.readString(), + EF_PATH_MF_SIM + EF_PATH_DF_TELECOM + EF_PATH_DF_PHONEBOOK); + } + + // p1. + equal(this.readInt32(), RECORD_NUMBER); + + // p2. + equal(this.readInt32(), READ_RECORD_ABSOLUTE_MODE); + + // p3. + equal(this.readInt32(), RECORD_SIZE); + + // data. + let strLen = this.readInt32(); + // Extension record + for (let i = 0; i < RECORD_SIZE; i++) { + equal(pduHelper.readHexOctet(), 0xff); + } + + this.readStringDelimiter(strLen); + + // pin2. + equal(this.readString(), null); + + // AID. Ignore because it's from modem. + this.readInt32(); + }; + + recordHelper.cleanEFRecord(fileId, RECORD_NUMBER); + } + + ril.appType = CARD_APPTYPE_SIM; + do_test(ICC_EF_EXT1); + + run_next_test(); +}); + +/** + * Verify ICCRecordHelper.getADNLikeExtensionRecordNumber + */ +add_test(function test_get_adn_like_extension_record_number() { + const RECORD_SIZE = 0x20; + + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let record = context.ICCRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + + function do_test(rawEF, expectedRecordNumber) { + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size + buf.writeInt32(rawEF.length * 2); + + // Write ext + for (let i = 0; i < rawEF.length; i += 2) { + helper.writeHexOctet(parseInt(rawEF.substr(i, 2), 16)); + } + + // Write string delimiter + buf.writeStringDelimiter(rawEF.length); + options.recordSize = RECORD_SIZE; + if (options.callback) { + options.callback(options); + } + }; + + let isSuccess = false; + let successCb = function successCb(number) { + equal(number, expectedRecordNumber); + isSuccess = true; + }; + + let errorCb = function errorCb(errorMsg) { + do_print("Reading ADNLike failed, msg = " + errorMsg); + ok(false); + }; + + record.getADNLikeExtensionRecordNumber(ICC_EF_ADN, 1, successCb, errorCb); + ok(isSuccess); + } + + // Valid Extension, Alpha Id(Encoded with GSM 8 bit): "Contact001", + // Dialling Number: 99887766554433221100, Ext1: 0x01 + do_test("436f6e74616374303031ffffffffffffffff0b8199887766554433221100ff01", 0x01); + // Empty Extension, Alpha Id(Encoded with GSM 8 bit): "Contact001", Ext1: 0xff + do_test("436f6e74616374303031ffffffffffffffffffffffffffffffffffffffffffff", 0xff); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_icc_ICCUtilsHelper.js b/dom/system/gonk/tests/test_ril_worker_icc_ICCUtilsHelper.js new file mode 100644 index 000000000..b23d0b598 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_ICCUtilsHelper.js @@ -0,0 +1,326 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify ICCUtilsHelper.isICCServiceAvailable. + */ +add_test(function test_is_icc_service_available() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ICCUtilsHelper = context.ICCUtilsHelper; + let RIL = context.RIL; + + function test_table(sst, geckoService, simEnabled, usimEnabled) { + RIL.iccInfoPrivate.sst = sst; + RIL.appType = CARD_APPTYPE_SIM; + equal(ICCUtilsHelper.isICCServiceAvailable(geckoService), simEnabled); + RIL.appType = CARD_APPTYPE_USIM; + equal(ICCUtilsHelper.isICCServiceAvailable(geckoService), usimEnabled); + } + + test_table([0x08], "ADN", true, false); + test_table([0x08], "FDN", false, false); + test_table([0x08], "SDN", false, true); + + run_next_test(); +}); + +/** + * Verify ICCUtilsHelper.isGsm8BitAlphabet + */ +add_test(function test_is_gsm_8bit_alphabet() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ICCUtilsHelper = context.ICCUtilsHelper; + const langTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + const langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + + equal(ICCUtilsHelper.isGsm8BitAlphabet(langTable), true); + equal(ICCUtilsHelper.isGsm8BitAlphabet(langShiftTable), true); + equal(ICCUtilsHelper.isGsm8BitAlphabet("\uaaaa"), false); + + run_next_test(); +}); + +/** + * Verify ICCUtilsHelper.parsePbrTlvs + */ +add_test(function test_parse_pbr_tlvs() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + + let pbrTlvs = [ + {tag: ICC_USIM_TYPE1_TAG, + length: 0x0F, + value: [{tag: ICC_USIM_EFADN_TAG, + length: 0x03, + value: [0x4F, 0x3A, 0x02]}, + {tag: ICC_USIM_EFIAP_TAG, + length: 0x03, + value: [0x4F, 0x25, 0x01]}, + {tag: ICC_USIM_EFPBC_TAG, + length: 0x03, + value: [0x4F, 0x09, 0x04]}] + }, + {tag: ICC_USIM_TYPE2_TAG, + length: 0x05, + value: [{tag: ICC_USIM_EFEMAIL_TAG, + length: 0x03, + value: [0x4F, 0x50, 0x0B]}, + {tag: ICC_USIM_EFANR_TAG, + length: 0x03, + value: [0x4F, 0x11, 0x02]}, + {tag: ICC_USIM_EFANR_TAG, + length: 0x03, + value: [0x4F, 0x12, 0x03]}] + }, + {tag: ICC_USIM_TYPE3_TAG, + length: 0x0A, + value: [{tag: ICC_USIM_EFCCP1_TAG, + length: 0x03, + value: [0x4F, 0x3D, 0x0A]}, + {tag: ICC_USIM_EFEXT1_TAG, + length: 0x03, + value: [0x4F, 0x4A, 0x03]}] + }, + ]; + + let pbr = context.ICCUtilsHelper.parsePbrTlvs(pbrTlvs); + equal(pbr.adn.fileId, 0x4F3a); + equal(pbr.iap.fileId, 0x4F25); + equal(pbr.pbc.fileId, 0x4F09); + equal(pbr.email.fileId, 0x4F50); + equal(pbr.anr0.fileId, 0x4f11); + equal(pbr.anr1.fileId, 0x4f12); + equal(pbr.ccp1.fileId, 0x4F3D); + equal(pbr.ext1.fileId, 0x4F4A); + + run_next_test(); +}); + +/** + * Verify MCC and MNC parsing + */ +add_test(function test_mcc_mnc_parsing() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.ICCUtilsHelper; + + function do_test(imsi, mncLength, expectedMcc, expectedMnc) { + let result = helper.parseMccMncFromImsi(imsi, mncLength); + + if (!imsi) { + equal(result, null); + return; + } + + equal(result.mcc, expectedMcc); + equal(result.mnc, expectedMnc); + } + + // Test the imsi is null. + do_test(null, null, null, null); + + // Test MCC is Taiwan + do_test("466923202422409", 0x02, "466", "92"); + do_test("466923202422409", 0x03, "466", "923"); + do_test("466923202422409", null, "466", "92"); + + // Test MCC is US + do_test("310260542718417", 0x02, "310", "26"); + do_test("310260542718417", 0x03, "310", "260"); + do_test("310260542718417", null, "310", "260"); + + run_next_test(); +}); + +add_test(function test_get_network_name_from_icc() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let ICCUtilsHelper = context.ICCUtilsHelper; + + function testGetNetworkNameFromICC(operatorData, expectedResult) { + let result = ICCUtilsHelper.getNetworkNameFromICC(operatorData.mcc, + operatorData.mnc, + operatorData.lac); + + if (expectedResult == null) { + equal(result, expectedResult); + } else { + equal(result.fullName, expectedResult.longName); + equal(result.shortName, expectedResult.shortName); + } + } + + // Before EF_OPL and EF_PNN have been loaded. + testGetNetworkNameFromICC({mcc: "123", mnc: "456", lac: 0x1000}, null); + testGetNetworkNameFromICC({mcc: "321", mnc: "654", lac: 0x2000}, null); + + // Set HPLMN + RIL.iccInfo.mcc = "123"; + RIL.iccInfo.mnc = "456"; + + RIL.voiceRegistrationState = { + cell: { + gsmLocationAreaCode: 0x1000 + } + }; + RIL.operator = {}; + + // Set EF_PNN + RIL.iccInfoPrivate = { + PNN: [ + {"fullName": "PNN1Long", "shortName": "PNN1Short"}, + {"fullName": "PNN2Long", "shortName": "PNN2Short"}, + {"fullName": "PNN3Long", "shortName": "PNN3Short"}, + {"fullName": "PNN4Long", "shortName": "PNN4Short"}, + {"fullName": "PNN5Long", "shortName": "PNN5Short"}, + {"fullName": "PNN6Long", "shortName": "PNN6Short"}, + {"fullName": "PNN7Long", "shortName": "PNN7Short"}, + {"fullName": "PNN8Long", "shortName": "PNN8Short"} + ] + }; + + // EF_OPL isn't available + ICCUtilsHelper.isICCServiceAvailable = function fakeIsICCServiceAvailable(service) { + return false; + }; + + // EF_OPL isn't available and current isn't in HPLMN, + testGetNetworkNameFromICC({mcc: "321", mnc: "654", lac: 0x1000}, null); + + // EF_OPL isn't available and current is in HPLMN, + // the first record of PNN should be returned. + testGetNetworkNameFromICC({mcc: "123", mnc: "456", lac: 0x1000}, + {longName: "PNN1Long", shortName: "PNN1Short"}); + + // EF_OPL is available + ICCUtilsHelper.isICCServiceAvailable = function fakeIsICCServiceAvailable(service) { + return service === "OPL"; + }; + + // Set EF_OPL + RIL.iccInfoPrivate.OPL = [ + { + "mcc": "123", + "mnc": "456", + "lacTacStart": 0, + "lacTacEnd": 0xFFFE, + "pnnRecordId": 4 + }, + { + "mcc": "321", + "mnc": "654", + "lacTacStart": 0, + "lacTacEnd": 0x0010, + "pnnRecordId": 3 + }, + { + "mcc": "321", + "mnc": "654", + "lacTacStart": 0x0100, + "lacTacEnd": 0x1010, + "pnnRecordId": 2 + }, + { + "mcc": ";;;", + "mnc": "01", + "lacTacStart": 0, + "lacTacEnd": 0xFFFE, + "pnnRecordId": 5 + }, + { + "mcc": "00;", + "mnc": "02", + "lacTacStart": 0, + "lacTacEnd": 0xFFFE, + "pnnRecordId": 6 + }, + { + "mcc": "001", + "mnc": ";;", + "lacTacStart": 0, + "lacTacEnd": 0xFFFE, + "pnnRecordId": 7 + }, + { + "mcc": "002", + "mnc": "0;", + "lacTacStart": 0, + "lacTacEnd": 0xFFFE, + "pnnRecordId": 8 + } + ]; + + // Both EF_PNN and EF_OPL are presented, and current PLMN is HPLMN, + testGetNetworkNameFromICC({mcc: "123", mnc: "456", lac: 0x1000}, + {longName: "PNN4Long", shortName: "PNN4Short"}); + + // Current PLMN is not HPLMN, and according to LAC, we should get + // the second PNN record. + testGetNetworkNameFromICC({mcc: "321", mnc: "654", lac: 0x1000}, + {longName: "PNN2Long", shortName: "PNN2Short"}); + + // Current PLMN is not HPLMN, and according to LAC, we should get + // the thrid PNN record. + testGetNetworkNameFromICC({mcc: "321", mnc: "654", lac: 0x0001}, + {longName: "PNN3Long", shortName: "PNN3Short"}); + + // Current PLMN is not HPLMN, and according to LAC, we should get + // the 5th PNN record after wild char (ie: ';') handling. + testGetNetworkNameFromICC({mcc: "001", mnc: "01", lac: 0x0001}, + {longName: "PNN5Long", shortName: "PNN5Short"}); + + // Current PLMN is not HPLMN, and according to LAC, we should get + // the 6th PNN record after wild char (ie: ';') handling. + testGetNetworkNameFromICC({mcc: "001", mnc: "02", lac: 0x0001}, + {longName: "PNN6Long", shortName: "PNN6Short"}); + + // Current PLMN is not HPLMN, and according to LAC, we should get + // the 7th PNN record after wild char (ie: ';') handling. + testGetNetworkNameFromICC({mcc: "001", mnc: "03", lac: 0x0001}, + {longName: "PNN7Long", shortName: "PNN7Short"}); + + // Current PLMN is not HPLMN, and according to LAC, we should get + // the 8th PNN record after wild char (ie: ';') handling. + testGetNetworkNameFromICC({mcc: "002", mnc: "03", lac: 0x0001}, + {longName: "PNN8Long", shortName: "PNN8Short"}); + + run_next_test(); +}); + +/** + * Verify ICCUtilsHelper.isCphsServiceAvailable. + */ +add_test(function test_is_cphs_service_available() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ICCUtilsHelper = context.ICCUtilsHelper; + let RIL = context.RIL; + RIL.iccInfoPrivate.cphsSt = new Uint8Array(2); + + function test_table(cphsSt, geckoService) { + RIL.iccInfoPrivate.cphsSt.set(cphsSt); + + for (let service in GECKO_ICC_SERVICES.cphs) { + equal(ICCUtilsHelper.isCphsServiceAvailable(service), + geckoService == service); + } + } + + test_table([0x03, 0x00], "CSP"); + test_table([0x0C, 0x00], "SST"); + test_table([0x30, 0x00], "MBN"); + test_table([0xC0, 0x00], "ONSF"); + test_table([0x00, 0x03], "INFO_NUM"); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_icc_IconLoader.js b/dom/system/gonk/tests/test_ril_worker_icc_IconLoader.js new file mode 100644 index 000000000..8bcd26ffe --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_IconLoader.js @@ -0,0 +1,771 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify IconLoader.loadIcons with recordNumbers array length being 1. + * Query images of one record at a time. + */ +add_test(function test_load_icon_basic() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let iconLoader = context.IconLoader; + let simRecordHelper = context.SimRecordHelper; + + let test_data = [ + {rawData: [ + {codingScheme: ICC_IMG_CODING_SCHEME_BASIC, + width: 0x10, + height: 0x10, + body: [0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x9d, 0xe9, 0xa1, 0x2d, 0xa1, 0x2d, 0xa1, 0x2b, + 0xa1, 0x2b, 0x9d, 0xe9, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff]}], + expected: [ + {width: 0x10, + height: 0x10, + pixels: [/* 1st byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 2nd byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 3rd byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 4th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 5th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 6th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 7th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 8th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 9th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 10th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 11th byte of body: 0x9d */ + 0xffffffff, 0x000000ff, 0x000000ff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 12th byte of body: 0xe9 */ + 0xffffffff, 0xffffffff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 13th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 14th byte of body: 0x2d */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 15th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 16th byte of body: 0x2d */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 17th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 18th byte of body: 0x2b */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0xffffffff, 0xffffffff, + /* 19th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 20th byte of body: 0x2b */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0xffffffff, 0xffffffff, + /* 21th byte of body: 0x9d */ + 0xffffffff, 0x000000ff, 0x000000ff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 22th byte of body: 0xe9 */ + 0xffffffff, 0xffffffff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 23th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 24th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 25th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 26th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 27th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 28th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 29th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 30th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 31th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 32th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff]}]}, + {rawData: [ + {codingScheme: ICC_IMG_CODING_SCHEME_COLOR, + width: 0x10, + height: 0x10, + bitsPerImgPoint: 0x04, + numOfClutEntries: 0x10, + body: [0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xcf, 0xfc, 0xcc, 0xfc, 0xcc, + 0xcf, 0xcf, 0xfc, 0xcf, 0xcf, 0xff, 0xfc, 0xff, 0xcf, + 0xcc, 0xfc, 0xcf, 0xcf, 0xff, 0xfc, 0xff, 0xcf, 0xcc, + 0xfc, 0xcf, 0xcf, 0xff, 0xfc, 0xff, 0xcf, 0xcf, 0xcc, + 0xcf, 0xcf, 0xff, 0xfc, 0xff, 0xcf, 0xcf, 0xcc, 0xcf, + 0xfc, 0xcc, 0xfc, 0xcc, 0xcf, 0xcf, 0xfc, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99], + clut: [0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, + 0x80, 0x80, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x80, + 0x00, 0x80, 0x80, 0xc0, 0xc0, 0xc0, 0x80, 0x80, 0x80, + 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff]}], + expected: [ + {width: 0x10, + height: 0x10, + pixels: [0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0xffffffff, 0xffffffff, + 0x0000ffff, 0x0000ffff, 0xffffffff, 0x0000ffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0x0000ffff, 0xffffffff, + 0xffffffff, 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, + 0xffffffff, 0x0000ffff, 0x0000ffff, 0xffffffff, 0x0000ffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x0000ffff, + 0xffffffff, 0xffffffff, 0x0000ffff, 0xffffffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0x0000ffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, 0x0000ffff, + 0xffffffff, 0x0000ffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x0000ffff, 0xffffffff, 0xffffffff, 0x0000ffff, + 0xffffffff, 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0xffffffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff]}]}, + {rawData: [ + {codingScheme: ICC_IMG_CODING_SCHEME_COLOR, + width: 0x03, + height: 0x03, + bitsPerImgPoint: 0x05, + numOfClutEntries: 0x20, + body: [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88], + clut: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, + 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, + 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, + 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, + 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f]}, + {codingScheme: ICC_IMG_CODING_SCHEME_BASIC, + width: 0x10, + height: 0x10, + body: [0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x9d, 0xe9, 0xa1, 0x2d, 0xa1, 0x2d, 0xa1, 0x2b, + 0xa1, 0x2b, 0x9d, 0xe9, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff]}], + expected: [ + {width: 0x03, + height: 0x03, + pixels: [0x000102ff, 0x060708ff, 0x0c0d0eff, 0x121314ff, 0x18191aff, + 0x1e1f20ff, 0x242526ff, 0x2a2b2cff, 0x333435ff]}, + {width: 0x10, + height: 0x10, + pixels: [/* 1st byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 2nd byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 3rd byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 4th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 5th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 6th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 7th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 8th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 9th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 10th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 11th byte of body: 0x9d */ + 0xffffffff, 0x000000ff, 0x000000ff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 12th byte of body: 0xe9 */ + 0xffffffff, 0xffffffff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 13th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 14th byte of body: 0x2d */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 15th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 16th byte of body: 0x2d */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 17th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 18th byte of body: 0x2b */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0xffffffff, 0xffffffff, + /* 19th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 20th byte of body: 0x2b */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0xffffffff, 0xffffffff, + /* 21th byte of body: 0x9d */ + 0xffffffff, 0x000000ff, 0x000000ff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 22th byte of body: 0xe9 */ + 0xffffffff, 0xffffffff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 23th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 24th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 25th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 26th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 27th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 28th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 29th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 30th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 31th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 32th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff]}]}, + {rawData: [ + {codingScheme: ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY, + width: 0x04, + height: 0x04, + bitsPerImgPoint: 0x04, + numOfClutEntries: 0x10, + body: [0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88], + clut: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, + 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, + 0x2d, 0x2e, 0x2f]}], + expected: [ + {width: 0x04, + height: 0x04, + pixels: [0x00000000, 0x00000000, 0x2a2b2cff, 0x2a2b2cff, 0x272829ff, + 0x272829ff, 0x242526ff, 0x242526ff, 0x212223ff, 0x212223ff, + 0x1e1f20ff, 0x1e1f20ff, 0x1b1c1dff, 0x1b1c1dff, 0x18191aff, + 0x18191aff]}]}]; + + function do_test(test_data, expected) { + simRecordHelper.readIMG = function fakeReadIMG(recordNumber, onsuccess, onerror) { + onsuccess(test_data); + }; + + let onsuccess = function(icons) { + // Query one record at a time. + equal(icons.length, 1); + equal(icons[0].length, expected.length); + for (let i = 0; i < icons[0].length; i++) { + // Read the i_th image of the record. + let icon = icons[0][i]; + let exp = expected[i]; + equal(icon.width, exp.width); + equal(icon.height, exp.height); + equal(icon.pixels.length, exp.pixels.length); + for (let j = 0; j < icon.pixels.length; j++) { + equal(icon.pixels[j], exp.pixels[j]); + } + } + }; + + iconLoader.loadIcons([0], onsuccess); + } + + for (let i = 0; i < test_data.length; i++) { + do_test(test_data[i].rawData, test_data[i].expected); + } + + run_next_test(); +}); + +/** + * Verify IconLoader.loadIcons. + */ +add_test(function test_load_icons() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let iconLoader = context.IconLoader; + let simRecordHelper = context.SimRecordHelper; + + let test_data = { + rawData: [ + // Record 1. + [{codingScheme: ICC_IMG_CODING_SCHEME_BASIC, + width: 0x10, + height: 0x10, + body: [0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x9d, 0xe9, 0xa1, 0x2d, 0xa1, 0x2d, 0xa1, 0x2b, + 0xa1, 0x2b, 0x9d, 0xe9, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff]}], + // Record 2. + [{codingScheme: ICC_IMG_CODING_SCHEME_COLOR, + width: 0x10, + height: 0x10, + bitsPerImgPoint: 0x04, + numOfClutEntries: 0x10, + body: [0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xcf, 0xfc, 0xcc, 0xfc, 0xcc, + 0xcf, 0xcf, 0xfc, 0xcf, 0xcf, 0xff, 0xfc, 0xff, 0xcf, + 0xcc, 0xfc, 0xcf, 0xcf, 0xff, 0xfc, 0xff, 0xcf, 0xcc, + 0xfc, 0xcf, 0xcf, 0xff, 0xfc, 0xff, 0xcf, 0xcf, 0xcc, + 0xcf, 0xcf, 0xff, 0xfc, 0xff, 0xcf, 0xcf, 0xcc, 0xcf, + 0xfc, 0xcc, 0xfc, 0xcc, 0xcf, 0xcf, 0xfc, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + 0xaa, 0xaa, 0xaa, 0xaa, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0x99], + clut: [0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, + 0x80, 0x80, 0x00, 0x00, 0x00, 0x80, 0x80, 0x00, 0x80, + 0x00, 0x80, 0x80, 0xc0, 0xc0, 0xc0, 0x80, 0x80, 0x80, + 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00, 0xff, 0xff, + 0xff, 0xff, 0xff]}], + // Record 3. + [{codingScheme: ICC_IMG_CODING_SCHEME_COLOR, + width: 0x03, + height: 0x03, + bitsPerImgPoint: 0x05, + numOfClutEntries: 0x20, + body: [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88], + clut: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, + 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, + 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, + 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, + 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f]}, + {codingScheme: ICC_IMG_CODING_SCHEME_BASIC, + width: 0x10, + height: 0x10, + body: [0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x00, 0x9d, 0xe9, 0xa1, 0x2d, 0xa1, 0x2d, 0xa1, 0x2b, + 0xa1, 0x2b, 0x9d, 0xe9, 0x00, 0x00, 0xff, 0xff, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff]}], + // Record 4. + [{codingScheme: ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY, + width: 0x04, + height: 0x04, + bitsPerImgPoint: 0x04, + numOfClutEntries: 0x10, + body: [0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88], + clut: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, + 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, + 0x2d, 0x2e, 0x2f]}]], + expected: [ + // Record 1. + [{width: 0x10, + height: 0x10, + pixels: [/* 1st byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 2nd byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 3rd byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 4th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 5th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 6th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 7th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 8th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 9th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 10th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 11th byte of body: 0x9d */ + 0xffffffff, 0x000000ff, 0x000000ff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 12th byte of body: 0xe9 */ + 0xffffffff, 0xffffffff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 13th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 14th byte of body: 0x2d */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 15th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 16th byte of body: 0x2d */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 17th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 18th byte of body: 0x2b */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0xffffffff, 0xffffffff, + /* 19th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 20th byte of body: 0x2b */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0xffffffff, 0xffffffff, + /* 21th byte of body: 0x9d */ + 0xffffffff, 0x000000ff, 0x000000ff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 22th byte of body: 0xe9 */ + 0xffffffff, 0xffffffff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 23th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 24th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 25th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 26th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 27th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 28th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 29th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 30th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 31th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 32th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff]}], + // Record 2. + [{width: 0x10, + height: 0x10, + pixels: [0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0xffffffff, 0xffffffff, + 0x0000ffff, 0x0000ffff, 0xffffffff, 0x0000ffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0x0000ffff, 0xffffffff, + 0xffffffff, 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, + 0xffffffff, 0x0000ffff, 0x0000ffff, 0xffffffff, 0x0000ffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x0000ffff, + 0xffffffff, 0xffffffff, 0x0000ffff, 0xffffffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0x0000ffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, 0x0000ffff, + 0xffffffff, 0x0000ffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x0000ffff, 0xffffffff, 0xffffffff, 0x0000ffff, + 0xffffffff, 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0x0000ffff, 0x0000ffff, + 0x0000ffff, 0xffffffff, 0x0000ffff, 0xffffffff, 0xffffffff, + 0x0000ffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, + 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0x00ff00ff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, 0xff0000ff, + 0xff0000ff]}], + // Record 3. + [{width: 0x03, + height: 0x03, + pixels: [0x000102ff, 0x060708ff, 0x0c0d0eff, 0x121314ff, 0x18191aff, + 0x1e1f20ff, 0x242526ff, 0x2a2b2cff, 0x333435ff]}, + {width: 0x10, + height: 0x10, + pixels: [/* 1st byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 2nd byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 3rd byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 4th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 5th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 6th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 7th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 8th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 9th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 10th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 11th byte of body: 0x9d */ + 0xffffffff, 0x000000ff, 0x000000ff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 12th byte of body: 0xe9 */ + 0xffffffff, 0xffffffff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 13th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 14th byte of body: 0x2d */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 15th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 16th byte of body: 0x2d */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 17th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 18th byte of body: 0x2b */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0xffffffff, 0xffffffff, + /* 19th byte of body: 0xa1 */ + 0xffffffff, 0x000000ff, 0xffffffff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 20th byte of body: 0x2b */ + 0x000000ff, 0x000000ff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0xffffffff, 0xffffffff, + /* 21th byte of body: 0x9d */ + 0xffffffff, 0x000000ff, 0x000000ff, 0xffffffff, 0xffffffff, + 0xffffffff, 0x000000ff, 0xffffffff, + /* 22th byte of body: 0xe9 */ + 0xffffffff, 0xffffffff, 0xffffffff, 0x000000ff, 0xffffffff, + 0x000000ff, 0x000000ff, 0xffffffff, + /* 23th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 24th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 25th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 26th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 27th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 28th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 29th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 30th byte of body: 0x00 */ + 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, 0x000000ff, + 0x000000ff, 0x000000ff, 0x000000ff, + /* 31th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff, + /* 32th byte of body: 0xff */ + 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, + 0xffffffff, 0xffffffff, 0xffffffff]}], + // Record 4. + [{width: 0x04, + height: 0x04, + pixels: [0x00000000, 0x00000000, 0x2a2b2cff, 0x2a2b2cff, 0x272829ff, + 0x272829ff, 0x242526ff, 0x242526ff, 0x212223ff, 0x212223ff, + 0x1e1f20ff, 0x1e1f20ff, 0x1b1c1dff, 0x1b1c1dff, 0x18191aff, + 0x18191aff]}]]}; + + function do_test() { + simRecordHelper.readIMG = function fakeReadIMG(recordNumber, onsuccess, onerror) { + onsuccess(test_data.rawData[recordNumber]); + }; + + let onsuccess = function(icons) { + equal(icons.length, test_data.expected.length); + for (let i = 0; i < icons.length; i++) { + for (let j = 0; j < icons[i].length; j++) { + // Read the j_th image from the i_th record. + let icon = icons[i][j]; + let expected = test_data.expected[i][j]; + equal(icon.width, expected.width); + equal(icon.height, expected.height); + equal(icon.pixels.length, expected.pixels.length); + for (let k = 0; k < icon.pixels.length; k++) { + equal(icon.pixels[k], expected.pixels[k]); + } + } + } + }; + + let recordNumbers = [0, 1, 2, 3]; + iconLoader.loadIcons(recordNumbers, onsuccess); + } + + do_test(); + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_icc_SimRecordHelper.js b/dom/system/gonk/tests/test_ril_worker_icc_SimRecordHelper.js new file mode 100644 index 000000000..6500cc663 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_icc_SimRecordHelper.js @@ -0,0 +1,1648 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify reading EF_AD and parsing MCC/MNC + */ +add_test(function test_reading_ad_and_parsing_mcc_mnc() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let helper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + function do_test(mncLengthInEf, imsi, expectedMcc, expectedMnc) { + ril.iccInfoPrivate.imsi = imsi; + + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + let ad = [0x00, 0x00, 0x00]; + if (typeof mncLengthInEf === 'number') { + ad.push(mncLengthInEf); + } + + // Write data size + buf.writeInt32(ad.length * 2); + + // Write data + for (let i = 0; i < ad.length; i++) { + helper.writeHexOctet(ad[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(ad.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + record.readAD(); + + equal(ril.iccInfo.mcc, expectedMcc); + equal(ril.iccInfo.mnc, expectedMnc); + } + + do_test(undefined, "466923202422409", "466", "92" ); + do_test(0x00, "466923202422409", "466", "92" ); + do_test(0x01, "466923202422409", "466", "92" ); + do_test(0x02, "466923202422409", "466", "92" ); + do_test(0x03, "466923202422409", "466", "923"); + do_test(0x04, "466923202422409", "466", "92" ); + do_test(0xff, "466923202422409", "466", "92" ); + + do_test(undefined, "310260542718417", "310", "260"); + do_test(0x00, "310260542718417", "310", "260"); + do_test(0x01, "310260542718417", "310", "260"); + do_test(0x02, "310260542718417", "310", "26" ); + do_test(0x03, "310260542718417", "310", "260"); + do_test(0x04, "310260542718417", "310", "260"); + do_test(0xff, "310260542718417", "310", "260"); + + run_next_test(); +}); + +add_test(function test_reading_optional_efs() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let gsmPdu = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + function buildSST(supportedEf) { + let sst = []; + let len = supportedEf.length; + for (let i = 0; i < len; i++) { + let index, bitmask, iccService; + if (ril.appType === CARD_APPTYPE_SIM) { + iccService = GECKO_ICC_SERVICES.sim[supportedEf[i]]; + iccService -= 1; + index = Math.floor(iccService / 4); + bitmask = 2 << ((iccService % 4) << 1); + } else if (ril.appType === CARD_APPTYPE_USIM){ + iccService = GECKO_ICC_SERVICES.usim[supportedEf[i]]; + iccService -= 1; + index = Math.floor(iccService / 8); + bitmask = 1 << ((iccService % 8) << 0); + } + + if (sst) { + sst[index] |= bitmask; + } + } + return sst; + } + + ril.updateCellBroadcastConfig = function fakeUpdateCellBroadcastConfig() { + // Ignore updateCellBroadcastConfig after reading SST + }; + + function do_test(sst, supportedEf) { + // Clone supportedEf to local array for testing + let testEf = supportedEf.slice(0); + + record.readMSISDN = function fakeReadMSISDN() { + testEf.splice(testEf.indexOf("MSISDN"), 1); + }; + + record.readMBDN = function fakeReadMBDN() { + testEf.splice(testEf.indexOf("MDN"), 1); + }; + + record.readMWIS = function fakeReadMWIS() { + testEf.splice(testEf.indexOf("MWIS"), 1); + }; + + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // Write data size + buf.writeInt32(sst.length * 2); + + // Write data + for (let i = 0; i < sst.length; i++) { + gsmPdu.writeHexOctet(sst[i] || 0); + } + + // Write string delimiter + buf.writeStringDelimiter(sst.length * 2); + + if (options.callback) { + options.callback(options); + } + + if (testEf.length !== 0) { + do_print("Un-handled EF: " + JSON.stringify(testEf)); + ok(false); + } + }; + + record.readSST(); + } + + // TODO: Add all necessary optional EFs eventually + let supportedEf = ["MSISDN", "MDN", "MWIS"]; + ril.appType = CARD_APPTYPE_SIM; + do_test(buildSST(supportedEf), supportedEf); + ril.appType = CARD_APPTYPE_USIM; + do_test(buildSST(supportedEf), supportedEf); + + run_next_test(); +}); + +/** + * Verify fetchSimRecords. + */ +add_test(function test_fetch_sim_records() { + let worker = newWorker(); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let iccRecord = context.ICCRecordHelper; + let simRecord = context.SimRecordHelper; + + function testFetchSimRecordes(expectCalled, expectCphsSuccess) { + let ifCalled = []; + + RIL.getIMSI = function() { + ifCalled.push("getIMSI"); + }; + + simRecord.readAD = function() { + ifCalled.push("readAD"); + }; + + simRecord.readCphsInfo = function(onsuccess, onerror) { + ifCalled.push("readCphsInfo"); + if (expectCphsSuccess) { + onsuccess(); + } else { + onerror(); + } + }; + + simRecord.readSST = function() { + ifCalled.push("readSST"); + }; + + simRecord.fetchSimRecords(); + + for (let i = 0; i < expectCalled.length; i++ ) { + if (ifCalled[i] != expectCalled[i]) { + do_print(expectCalled[i] + " is not called."); + ok(false); + } + } + } + + let expectCalled = ["getIMSI", "readAD", "readCphsInfo", "readSST"]; + testFetchSimRecordes(expectCalled, true); + testFetchSimRecordes(expectCalled, false); + + run_next_test(); +}); + +/** + * Verify SimRecordHelper.readMWIS + */ +add_test(function test_read_mwis() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let recordHelper = context.SimRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + let mwisData; + let postedMessage; + + worker.postMessage = function fakePostMessage(message) { + postedMessage = message; + }; + + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + if (mwisData) { + // Write data size + buf.writeInt32(mwisData.length * 2); + + // Write MWIS + for (let i = 0; i < mwisData.length; i++) { + helper.writeHexOctet(mwisData[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(mwisData.length * 2); + + options.recordSize = mwisData.length; + if (options.callback) { + options.callback(options); + } + } else { + do_print("mwisData[] is not set."); + } + }; + + function buildMwisData(isActive, msgCount) { + if (msgCount < 0 || msgCount === GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN) { + msgCount = 0; + } else if (msgCount > 255) { + msgCount = 255; + } + + mwisData = [ (isActive) ? 0x01 : 0x00, + msgCount, + 0xFF, 0xFF, 0xFF ]; + } + + function do_test(isActive, msgCount) { + buildMwisData(isActive, msgCount); + recordHelper.readMWIS(); + + equal("iccmwis", postedMessage.rilMessageType); + equal(isActive, postedMessage.mwi.active); + equal((isActive) ? msgCount : 0, postedMessage.mwi.msgCount); + } + + do_test(true, GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN); + do_test(true, 1); + do_test(true, 255); + + do_test(false, 0); + do_test(false, 255); // Test the corner case when mwi is disable with incorrect msgCount. + + run_next_test(); +}); + +/** + * Verify SimRecordHelper.updateMWIS + */ +add_test(function test_update_mwis() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let ril = context.RIL; + ril.appType = CARD_APPTYPE_USIM; + ril.iccInfoPrivate.mwis = [0x00, 0x00, 0x00, 0x00, 0x00]; + let recordHelper = context.SimRecordHelper; + let buf = context.Buf; + let ioHelper = context.ICCIOHelper; + let recordSize = ril.iccInfoPrivate.mwis.length; + let recordNum = 1; + + ioHelper.updateLinearFixedEF = function(options) { + options.pathId = context.ICCFileHelper.getEFPath(options.fileId); + options.command = ICC_COMMAND_UPDATE_RECORD; + options.p1 = options.recordNumber; + options.p2 = READ_RECORD_ABSOLUTE_MODE; + options.p3 = recordSize; + ril.iccIO(options); + }; + + function do_test(isActive, count) { + let mwis = ril.iccInfoPrivate.mwis; + let isUpdated = false; + + function buildMwisData() { + let result = mwis.slice(0); + result[0] = isActive? (mwis[0] | 0x01) : (mwis[0] & 0xFE); + result[1] = (count === GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN) ? 0 : count; + + return result; + } + + buf.sendParcel = function() { + isUpdated = true; + + // Request Type. + equal(this.readInt32(), REQUEST_SIM_IO); + + // Token : we don't care + this.readInt32(); + + // command. + equal(this.readInt32(), ICC_COMMAND_UPDATE_RECORD); + + // fileId. + equal(this.readInt32(), ICC_EF_MWIS); + + // pathId. + equal(this.readString(), + EF_PATH_MF_SIM + ((ril.appType === CARD_APPTYPE_USIM) ? EF_PATH_ADF_USIM : EF_PATH_DF_GSM)); + + // p1. + equal(this.readInt32(), recordNum); + + // p2. + equal(this.readInt32(), READ_RECORD_ABSOLUTE_MODE); + + // p3. + equal(this.readInt32(), recordSize); + + // data. + let strLen = this.readInt32(); + equal(recordSize * 2, strLen); + let expectedMwis = buildMwisData(); + for (let i = 0; i < recordSize; i++) { + equal(expectedMwis[i], pduHelper.readHexOctet()); + } + this.readStringDelimiter(strLen); + + // pin2. + equal(this.readString(), null); + + // AID. Ignore because it's from modem. + this.readInt32(); + }; + + ok(!isUpdated); + + recordHelper.updateMWIS({ active: isActive, + msgCount: count }); + + ok((ril.iccInfoPrivate.mwis) ? isUpdated : !isUpdated); + } + + do_test(true, GECKO_VOICEMAIL_MESSAGE_COUNT_UNKNOWN); + do_test(true, 1); + do_test(true, 255); + + do_test(false, 0); + + // Test if Path ID is correct for SIM. + ril.appType = CARD_APPTYPE_SIM; + do_test(false, 0); + + // Test if loadLinearFixedEF() is not invoked in updateMWIS() when + // EF_MWIS is not loaded/available. + delete ril.iccInfoPrivate.mwis; + do_test(false, 0); + + run_next_test(); +}); + +/** + * Verify the call flow of receiving Class 2 SMS stored in SIM: + * 1. UNSOLICITED_RESPONSE_NEW_SMS_ON_SIM. + * 2. SimRecordHelper.readSMS(). + * 3. sendChromeMessage() with rilMessageType == "sms-received". + */ +add_test(function test_read_new_sms_on_sim() { + // Instead of reusing newUint8Worker defined in this file, + // we define our own worker to fake the methods in WorkerBuffer dynamically. + function newSmsOnSimWorkerHelper() { + let _postedMessage; + let _worker = newWorker({ + postRILMessage: function(data) { + }, + postMessage: function(message) { + _postedMessage = message; + } + }); + + _worker.debug = do_print; + + return { + get postedMessage() { + return _postedMessage; + }, + get worker() { + return _worker; + }, + fakeWokerBuffer: function() { + let context = _worker.ContextPool._contexts[0]; + let index = 0; // index for read + let buf = []; + context.Buf.writeUint8 = function(value) { + buf.push(value); + }; + context.Buf.readUint8 = function() { + return buf[index++]; + }; + context.Buf.seekIncoming = function(offset) { + index += offset; + }; + context.Buf.getReadAvailable = function() { + return buf.length - index; + }; + } + }; + } + + let workerHelper = newSmsOnSimWorkerHelper(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.ICCIOHelper.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // SimStatus: Unread, SMSC:+0123456789, Sender: +9876543210, Text: How are you? + let SimSmsPduHex = "0306911032547698040A9189674523010000208062917314080CC8F71D14969741F977FD07" + // In 4.2.25 EF_SMS Short Messages of 3GPP TS 31.102: + // 1. Record length == 176 bytes. + // 2. Any bytes in the record following the TPDU shall be filled with 'FF'. + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + + workerHelper.fakeWokerBuffer(); + + context.Buf.writeString(SimSmsPduHex); + + options.recordSize = 176; // Record length is fixed to 176 bytes. + if (options.callback) { + options.callback(options); + } + }; + + function newSmsOnSimParcel() { + let data = new Uint8Array(4 + 4); // Int32List with 1 element. + let offset = 0; + + function writeInt(value) { + data[offset++] = value & 0xFF; + data[offset++] = (value >> 8) & 0xFF; + data[offset++] = (value >> 16) & 0xFF; + data[offset++] = (value >> 24) & 0xFF; + } + + writeInt(1); // Length of Int32List + writeInt(1); // RecordNum = 1. + + return newIncomingParcel(-1, + RESPONSE_TYPE_UNSOLICITED, + UNSOLICITED_RESPONSE_NEW_SMS_ON_SIM, + data); + } + + function do_test() { + worker.onRILMessage(0, newSmsOnSimParcel()); + + let postedMessage = workerHelper.postedMessage; + + equal("sms-received", postedMessage.rilMessageType); + equal("+0123456789", postedMessage.SMSC); + equal("+9876543210", postedMessage.sender); + equal("How are you?", postedMessage.body); + } + + do_test(); + + run_next_test(); +}); + +/** + * Verify the result of updateDisplayCondition after reading EF_SPDI | EF_SPN. + */ +add_test(function test_update_display_condition() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let helper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + function do_test_spdi() { + // No EF_SPN, but having EF_SPDI. + // It implies "ril.iccInfoPrivate.spnDisplayCondition = undefined;". + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // PLMN lists are : 234-136 and 466-92. + let spdi = [0xA3, 0x0B, 0x80, 0x09, 0x32, 0x64, 0x31, 0x64, 0x26, 0x9F, + 0xFF, 0xFF, 0xFF]; + + // Write data size. + buf.writeInt32(spdi.length * 2); + + // Write data. + for (let i = 0; i < spdi.length; i++) { + helper.writeHexOctet(spdi[i]); + } + + // Write string delimiter. + buf.writeStringDelimiter(spdi.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + record.readSPDI(); + + equal(ril.iccInfo.isDisplayNetworkNameRequired, true); + equal(ril.iccInfo.isDisplaySpnRequired, false); + } + + function do_test_spn(displayCondition, + expectedPlmnNameDisplay, + expectedSpnDisplay) { + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // "Android" as Service Provider Name. + let spn = [0x41, 0x6E, 0x64, 0x72, 0x6F, 0x69, 0x64]; + if (typeof displayCondition === 'number') { + spn.unshift(displayCondition); + } + + // Write data size. + buf.writeInt32(spn.length * 2); + + // Write data. + for (let i = 0; i < spn.length; i++) { + helper.writeHexOctet(spn[i]); + } + + // Write string delimiter. + buf.writeStringDelimiter(spn.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + record.readSPN(); + + equal(ril.iccInfo.isDisplayNetworkNameRequired, expectedPlmnNameDisplay); + equal(ril.iccInfo.isDisplaySpnRequired, expectedSpnDisplay); + } + + // Create empty operator object. + ril.operator = {}; + // Setup SIM MCC-MNC to 310-260 as home network. + ril.iccInfo.mcc = 310; + ril.iccInfo.mnc = 260; + + do_test_spdi(); + + // No network. + do_test_spn(0x00, true, true); + do_test_spn(0x01, true, true); + do_test_spn(0x02, true, false); + do_test_spn(0x03, true, false); + + // Home network. + ril.operator.mcc = 310; + ril.operator.mnc = 260; + do_test_spn(0x00, false, true); + do_test_spn(0x01, true, true); + do_test_spn(0x02, false, true); + do_test_spn(0x03, true, true); + + // Not HPLMN but in PLMN list. + ril.iccInfoPrivate.SPDI = [{"mcc":"234","mnc":"136"},{"mcc":"466","mnc":"92"}]; + ril.operator.mcc = 466; + ril.operator.mnc = 92; + do_test_spn(0x00, false, true); + do_test_spn(0x01, true, true); + do_test_spn(0x02, false, true); + do_test_spn(0x03, true, true); + ril.iccInfoPrivate.SPDI = null; // reset SPDI to null; + + // Non-Home network. + ril.operator.mcc = 466; + ril.operator.mnc = 01; + do_test_spn(0x00, true, true); + do_test_spn(0x01, true, true); + do_test_spn(0x02, true, false); + do_test_spn(0x03, true, false); + + run_next_test(); +}); + +/** + * Verify reading EF_IMG and EF_IIDF with ICC_IMG_CODING_SCHEME_BASIC + */ +add_test(function test_reading_img_basic() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let helper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + let test_data = [ + {img: [0x01, 0x05, 0x05, 0x11, 0x4f, 0x00, 0x00, 0x00, 0x00, 0x06], + iidf: [ + [/* Header */ + 0x05, 0x05, + /* Image body */ + 0x11, 0x33, 0x55, 0xfe]], + expected: [ + {width: 0x05, + height: 0x05, + codingScheme: ICC_IMG_CODING_SCHEME_BASIC, + body: [0x11, 0x33, 0x55, 0xfe]}]}, + {img: [0x01, 0x05, 0x05, 0x11, 0x4f, 0x00, 0x00, 0x00, 0x00, 0x06, + /* Padding */ + 0xff, 0xff], + iidf: [ + [/* Header */ + 0x05, 0x05, + /* Image body */ + 0x11, 0x33, 0x55, 0xfe]], + expected: [ + {width: 0x05, + height: 0x05, + codingScheme: ICC_IMG_CODING_SCHEME_BASIC, + body: [0x11, 0x33, 0x55, 0xfe]}]}, + {img: [0x02, 0x10, 0x01, 0x11, 0x4f, 0x04, 0x00, 0x05, 0x00, 0x04, 0x10, + 0x01, 0x11, 0x4f, 0x05, 0x00, 0x05, 0x00, 0x04], + iidf: [ + [/* Data offset */ + 0xff, 0xff, 0xff, 0xff, 0xff, + /* Header */ + 0x10, 0x01, + /* Image body */ + 0x11, 0x99, + /* Trailing data */ + 0xff, 0xff, 0xff], + [/* Data offset */ + 0xff, 0xff, 0xff, 0xff, 0xff, + /* Header */ + 0x10, 0x01, + /* Image body */ + 0x99, 0x11]], + expected: [ + {width: 0x10, + height: 0x01, + codingScheme: ICC_IMG_CODING_SCHEME_BASIC, + body: [0x11, 0x99]}, + {width: 0x10, + height: 0x01, + codingScheme: ICC_IMG_CODING_SCHEME_BASIC, + body: [0x99, 0x11]}]}, + {img: [0x01, 0x28, 0x20, 0x11, 0x4f, 0xac, 0x00, 0x0b, 0x00, 0xa2], + iidf: [ + [/* Data offset */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + /* Header */ + 0x28, 0x20, + /* Image body */ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, + 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, + 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, + 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, + 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, + 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, + 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x00, 0x01, 0x02, + 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, + 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f]], + expected: [ + {width: 0x28, + height: 0x20, + codingScheme: ICC_IMG_CODING_SCHEME_BASIC, + body: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, + 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, + 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, + 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x00, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, + 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, + 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f]}]}]; + + function do_test(img, iidf, expected) { + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size + buf.writeInt32(img.length * 2); + + // Write data + for (let i = 0; i < img.length; i++) { + helper.writeHexOctet(img[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(img.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + let instanceIndex = 0; + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // Write data size + buf.writeInt32(iidf[instanceIndex].length * 2); + + // Write data + for (let i = 0; i < iidf[instanceIndex].length; i++) { + helper.writeHexOctet(iidf[instanceIndex][i]); + } + + // Write string delimiter + buf.writeStringDelimiter(iidf[instanceIndex].length * 2); + + instanceIndex++; + + if (options.callback) { + options.callback(options); + } + }; + + let onsuccess = function(icons) { + equal(icons.length, expected.length); + for (let i = 0; i < icons.length; i++) { + let icon = icons[i]; + let exp = expected[i]; + equal(icon.width, exp.width); + equal(icon.height, exp.height); + equal(icon.codingScheme, exp.codingScheme); + + equal(icon.body.length, exp.body.length); + for (let j = 0; j < icon.body.length; j++) { + equal(icon.body[j], exp.body[j]); + } + } + }; + record.readIMG(0, onsuccess); + } + + for (let i = 0; i< test_data.length; i++) { + do_test(test_data[i].img, test_data[i].iidf, test_data[i].expected); + } + run_next_test(); +}); + +/** + * Verify reading EF_IMG and EF_IIDF with the case data length is not enough + */ +add_test(function test_reading_img_length_error() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let helper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + let test_data = [ + {/* Offset length not enough, should be 4. */ + img: [0x01, 0x05, 0x05, 0x11, 0x4f, 0x00, 0x00, 0x04, 0x00, 0x06], + iidf: [0xff, 0xff, 0xff, // Offset. + 0x05, 0x05, 0x11, 0x22, 0x33, 0xfe]}, + {/* iidf data length not enough, should be 6. */ + img: [0x01, 0x05, 0x05, 0x11, 0x4f, 0x00, 0x00, 0x00, 0x00, 0x06], + iidf: [0x05, 0x05, 0x11, 0x22, 0x33]}]; + + function do_test(img, iidf) { + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size + buf.writeInt32(img.length * 2); + + // Write data + for (let i = 0; i < img.length; i++) { + helper.writeHexOctet(img[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(img.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // Write data size + buf.writeInt32(iidf.length * 2); + + // Write data + for (let i = 0; i < iidf.length; i++) { + helper.writeHexOctet(iidf[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(iidf.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + let onsuccess = function() { + do_print("onsuccess shouldn't be called."); + ok(false); + }; + + let onerror = function() { + do_print("onerror called as expected."); + ok(true); + }; + + record.readIMG(0, onsuccess, onerror); + } + + for (let i = 0; i < test_data.length; i++) { + do_test(test_data[i].img, test_data[i].iidf); + } + run_next_test(); +}); + +/** + * Verify reading EF_IMG and EF_IIDF with an invalid fileId + */ +add_test(function test_reading_img_invalid_fileId() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let helper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + // Test invalid fileId: 0x5f00. + let img_test = [0x01, 0x05, 0x05, 0x11, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x06]; + let iidf_test = [0x05, 0x05, 0x11, 0x22, 0x33, 0xfe]; + + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size + buf.writeInt32(img_test.length * 2); + + // Write data + for (let i = 0; i < img_test.length; i++) { + helper.writeHexOctet(img_test[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(img_test.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // Write data size + buf.writeInt32(iidf_test.length * 2); + + // Write data + for (let i = 0; i < iidf_test.length; i++) { + helper.writeHexOctet(iidf_test[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(iidf_test.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + let onsuccess = function() { + do_print("onsuccess shouldn't be called."); + ok(false); + }; + + let onerror = function() { + do_print("onerror called as expected."); + ok(true); + }; + + record.readIMG(0, onsuccess, onerror); + + run_next_test(); +}); + +/** + * Verify reading EF_IMG with a wrong record length + */ +add_test(function test_reading_img_wrong_record_length() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let helper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + let test_data = [ + [0x01, 0x05, 0x05, 0x11, 0x4f, 0x00, 0x00, 0x00], + [0x02, 0x05, 0x05, 0x11, 0x4f, 0x00, 0x00, 0x00, 0x00, 0x06]]; + + function do_test(img) { + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size + buf.writeInt32(img.length * 2); + + // Write data + for (let i = 0; i < img.length; i++) { + helper.writeHexOctet(img[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(img.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + let onsuccess = function() { + do_print("onsuccess shouldn't be called."); + ok(false); + }; + + let onerror = function() { + do_print("onerror called as expected."); + ok(true); + }; + + record.readIMG(0, onsuccess, onerror); + } + + for (let i = 0; i < test_data.length; i++) { + do_test(test_data[i]); + } + run_next_test(); +}); + +/** + * Verify reading EF_IMG and EF_IIDF with ICC_IMG_CODING_SCHEME_COLOR + */ +add_test(function test_reading_img_color() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let helper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + let test_data = [ + {img: [0x01, 0x05, 0x05, 0x21, 0x4f, 0x11, 0x00, 0x00, 0x00, 0x13], + iidf: [ + [/* Header */ + 0x05, 0x05, 0x03, 0x08, 0x00, 0x13, + /* Image body */ + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, + 0xb0, 0xc0, 0xd0, + /* Clut entries */ + 0x00, 0x01, 0x02, + 0x10, 0x11, 0x12, + 0x20, 0x21, 0x22, + 0x30, 0x31, 0x32, + 0x40, 0x41, 0x42, + 0x50, 0x51, 0x52, + 0x60, 0x61, 0x62, + 0x70, 0x71, 0x72]], + expected: [ + {width: 0x05, + height: 0x05, + codingScheme: ICC_IMG_CODING_SCHEME_COLOR, + bitsPerImgPoint: 0x03, + numOfClutEntries: 0x08, + body: [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, 0xb0, + 0xc0, 0xd0], + clut: [0x00, 0x01, 0x02, + 0x10, 0x11, 0x12, + 0x20, 0x21, 0x22, + 0x30, 0x31, 0x32, + 0x40, 0x41, 0x42, + 0x50, 0x51, 0x52, + 0x60, 0x61, 0x62, + 0x70, 0x71, 0x72]}]}, + {img: [0x02, 0x01, 0x06, 0x21, 0x4f, 0x33, 0x00, 0x02, 0x00, 0x08, 0x01, + 0x06, 0x21, 0x4f, 0x44, 0x00, 0x02, 0x00, 0x08], + iidf: [ + [/* Data offset */ + 0xff, 0xff, + /* Header */ + 0x01, 0x06, 0x02, 0x04, 0x00, 0x0d, + /* Image body */ + 0x40, 0x50, + /* Clut offset */ + 0xaa, 0xbb, 0xcc, + /* Clut entries */ + 0x01, 0x03, 0x05, + 0x21, 0x23, 0x25, + 0x41, 0x43, 0x45, + 0x61, 0x63, 0x65], + [/* Data offset */ + 0xff, 0xff, + /* Header */ + 0x01, 0x06, 0x02, 0x04, 0x00, 0x0d, + /* Image body */ + 0x4f, 0x5f, + /* Clut offset */ + 0xaa, 0xbb, 0xcc, + /* Clut entries */ + 0x11, 0x13, 0x15, + 0x21, 0x23, 0x25, + 0x41, 0x43, 0x45, + 0x61, 0x63, 0x65]], + expected: [ + {width: 0x01, + height: 0x06, + codingScheme: ICC_IMG_CODING_SCHEME_COLOR, + bitsPerImgPoint: 0x02, + numOfClutEntries: 0x04, + body: [0x40, 0x50], + clut: [0x01, 0x03, 0x05, + 0x21, 0x23, 0x25, + 0x41, 0x43, 0x45, + 0x61, 0x63, 0x65]}, + {width: 0x01, + height: 0x06, + codingScheme: ICC_IMG_CODING_SCHEME_COLOR, + bitsPerImgPoint: 0x02, + numOfClutEntries: 0x04, + body: [0x4f, 0x5f], + clut: [0x11, 0x13, 0x15, + 0x21, 0x23, 0x25, + 0x41, 0x43, 0x45, + 0x61, 0x63, 0x65]}]}]; + + function do_test(img, iidf, expected) { + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size + buf.writeInt32(img.length * 2); + + // Write data + for (let i = 0; i < img.length; i++) { + helper.writeHexOctet(img[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(img.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + let instanceIndex = 0; + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // Write data size + buf.writeInt32(iidf[instanceIndex].length * 2); + + // Write data + for (let i = 0; i < iidf[instanceIndex].length; i++) { + helper.writeHexOctet(iidf[instanceIndex][i]); + } + + // Write string delimiter + buf.writeStringDelimiter(iidf[instanceIndex].length * 2); + + instanceIndex++; + + if (options.callback) { + options.callback(options); + } + }; + + let onsuccess = function(icons) { + equal(icons.length, expected.length); + for (let i = 0; i < icons.length; i++) { + let icon = icons[i]; + let exp = expected[i]; + equal(icon.width, exp.width); + equal(icon.height, exp.height); + equal(icon.codingScheme, exp.codingScheme); + + equal(icon.body.length, exp.body.length); + for (let j = 0; j < icon.body.length; j++) { + equal(icon.body[j], exp.body[j]); + } + + equal(icon.clut.length, exp.clut.length); + for (let j = 0; j < icon.clut.length; j++) { + equal(icon.clut[j], exp.clut[j]); + } + } + }; + + record.readIMG(0, onsuccess); + } + + for (let i = 0; i< test_data.length; i++) { + do_test(test_data[i].img, test_data[i].iidf, test_data[i].expected); + } + run_next_test(); +}); + +/** + * Verify reading EF_IMG and EF_IIDF with + * ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY + */ +add_test(function test_reading_img_color() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let helper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + let test_data = [ + {img: [0x01, 0x05, 0x05, 0x22, 0x4f, 0x11, 0x00, 0x00, 0x00, 0x13], + iidf: [ + [/* Header */ + 0x05, 0x05, 0x03, 0x08, 0x00, 0x13, + /* Image body */ + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, + 0xb0, 0xc0, 0xd0, + /* Clut entries */ + 0x00, 0x01, 0x02, + 0x10, 0x11, 0x12, + 0x20, 0x21, 0x22, + 0x30, 0x31, 0x32, + 0x40, 0x41, 0x42, + 0x50, 0x51, 0x52, + 0x60, 0x61, 0x62, + 0x70, 0x71, 0x72]], + expected: [ + {width: 0x05, + height: 0x05, + codingScheme: ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY, + bitsPerImgPoint: 0x03, + numOfClutEntries: 0x08, + body: [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, + 0xa0, 0xb0, 0xc0, 0xd0], + clut: [0x00, 0x01, 0x02, + 0x10, 0x11, 0x12, + 0x20, 0x21, 0x22, + 0x30, 0x31, 0x32, + 0x40, 0x41, 0x42, + 0x50, 0x51, 0x52, + 0x60, 0x61, 0x62, + 0x70, 0x71, 0x72]}]}, + {img: [0x02, 0x01, 0x06, 0x22, 0x4f, 0x33, 0x00, 0x02, 0x00, 0x08, 0x01, + 0x06, 0x22, 0x4f, 0x33, 0x00, 0x02, 0x00, 0x08], + iidf: [ + [/* Data offset */ + 0xff, 0xff, + /* Header */ + 0x01, 0x06, 0x02, 0x04, 0x00, 0x0d, + /* Image body */ + 0x40, 0x50, + /* Clut offset */ + 0x0a, 0x0b, 0x0c, + /* Clut entries */ + 0x01, 0x03, 0x05, + 0x21, 0x23, 0x25, + 0x41, 0x43, 0x45, + 0x61, 0x63, 0x65], + [/* Data offset */ + 0xff, 0xff, + /* Header */ + 0x01, 0x06, 0x02, 0x04, 0x00, 0x0d, + /* Image body */ + 0x4f, 0x5f, + /* Clut offset */ + 0x0a, 0x0b, 0x0c, + /* Clut entries */ + 0x11, 0x13, 0x15, + 0x21, 0x23, 0x25, + 0x41, 0x43, 0x45, + 0x61, 0x63, 0x65]], + expected: [ + {width: 0x01, + height: 0x06, + codingScheme: ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY, + bitsPerImgPoint: 0x02, + numOfClutEntries: 0x04, + body: [0x40, 0x50], + clut: [0x01, 0x03, 0x05, + 0x21, 0x23, 0x25, + 0x41, 0x43, 0x45, + 0x61, 0x63, 0x65]}, + {width: 0x01, + height: 0x06, + codingScheme: ICC_IMG_CODING_SCHEME_COLOR_TRANSPARENCY, + bitsPerImgPoint: 0x02, + numOfClutEntries: 0x04, + body: [0x4f, 0x5f], + clut: [0x11, 0x13, 0x15, + 0x21, 0x23, 0x25, + 0x41, 0x43, 0x45, + 0x61, 0x63, 0x65]}]}]; + + function do_test(img, iidf, expected) { + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size + buf.writeInt32(img.length * 2); + + // Write data + for (let i = 0; i < img.length; i++) { + helper.writeHexOctet(img[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(img.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + let instanceIndex = 0; + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // Write data size + buf.writeInt32(iidf[instanceIndex].length * 2); + + // Write data + for (let i = 0; i < iidf[instanceIndex].length; i++) { + helper.writeHexOctet(iidf[instanceIndex][i]); + } + + // Write string delimiter + buf.writeStringDelimiter(iidf[instanceIndex].length * 2); + + instanceIndex++; + + if (options.callback) { + options.callback(options); + } + }; + + let onsuccess = function(icons) { + equal(icons.length, expected.length); + for (let i = 0; i < icons.length; i++) { + let icon = icons[i]; + let exp = expected[i]; + equal(icon.width, exp.width); + equal(icon.height, exp.height); + equal(icon.codingScheme, exp.codingScheme); + + equal(icon.body.length, exp.body.length); + for (let j = 0; j < icon.body.length; j++) { + equal(icon.body[j], exp.body[j]); + } + + equal(icon.clut.length, exp.clut.length); + for (let j = 0; j < icon.clut.length; j++) { + equal(icon.clut[j], exp.clut[j]); + } + } + }; + + record.readIMG(0, onsuccess); + } + + for (let i = 0; i< test_data.length; i++) { + do_test(test_data[i].img, test_data[i].iidf, test_data[i].expected); + } + run_next_test(); +}); + +/** + * Verify SimRecordHelper.readCphsInfo + */ +add_test(function test_read_cphs_info() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let pduHelper = context.GsmPDUHelper; + let recordHelper = context.SimRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + let cphsPDU = new Uint8Array(3); + + io.loadTransparentEF = function(options) { + if (cphsPDU) { + // Write data size + buf.writeInt32(cphsPDU.length * 2); + + // Write CPHS INFO + for (let i = 0; i < cphsPDU.length; i++) { + pduHelper.writeHexOctet(cphsPDU[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(cphsPDU.length * 2); + + if (options.callback) { + options.callback(options); + } + } else { + do_print("cphsPDU[] is not set."); + } + }; + + function do_test(cphsInfo, cphsSt) { + let onsuccess = false; + let onerror = false; + + delete RIL.iccInfoPrivate.cphsSt; + cphsPDU.set(cphsInfo); + recordHelper.readCphsInfo(() => { onsuccess = true; }, + () => { onerror = true; }); + + ok((cphsSt) ? onsuccess : onerror); + ok(!((cphsSt) ? onerror : onsuccess)); + if (cphsSt) { + equal(RIL.iccInfoPrivate.cphsSt.length, cphsSt.length); + for (let i = 0; i < cphsSt.length; i++) { + equal(RIL.iccInfoPrivate.cphsSt[i], cphsSt[i]); + } + } else { + equal(RIL.iccInfoPrivate.cphsSt, cphsSt); + } + } + + do_test([ + 0x01, // Phase 1 + 0xFF, // All available & activated + 0x03 // All available & activated + ], + [ + 0x3F, // All services except ONSF(bit 8-7) are available and activated. + 0x00 // INFO_NUM shall not be available & activated. + ]); + + do_test([ + 0x02, // Phase 2 + 0xFF, // All available & activated + 0x03 // All available & activated + ], + [ + 0xF3, // All services except ONSF are available and activated. + 0x03 // INFO_NUM shall not be available & activated. + ]); + + do_test([ + 0x03, // Phase 3 + 0xFF, // All available & activated + 0x03 // All available & activated + ], + undefined); // RIL.iccInfoPrivate.cphsSt shall be remained as 'undefined'. + + run_next_test(); +}); + +/** + * Verify SimRecordHelper.readMBDN/SimRecordHelper.readCphsMBN + */ +add_test(function test_read_voicemail_number() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let pduHelper = context.GsmPDUHelper; + let recordHelper = context.SimRecordHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + let postedMessage; + + worker.postMessage = function(message) { + postedMessage = message; + }; + + io.loadLinearFixedEF = function(options) { + let mbnData = [ + 0x56, 0x6F, 0x69, 0x63, 0x65, 0x6D, 0x61, 0x69, + 0x6C, 0xFF, // Alpha Identifier: Voicemail + 0x03, // Length of BCD number: 3 + 0x80, // TOA: Unknown + 0x11, 0xF1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, // Dialing Number: 111 + 0xFF, // Capability/Configuration Record Identifier + 0xFF // Extension Record Identifier + ]; + + // Write data size + buf.writeInt32(mbnData.length * 2); + + // Write MBN + for (let i = 0; i < mbnData.length; i++) { + pduHelper.writeHexOctet(mbnData[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(mbnData.length * 2); + + options.recordSize = mbnData.length; + if (options.callback) { + options.callback(options); + } + }; + + function do_test(funcName, msgCount) { + postedMessage = null; + delete RIL.iccInfoPrivate.mbdn; + recordHelper[funcName](); + + equal("iccmbdn", postedMessage.rilMessageType); + equal("Voicemail", postedMessage.alphaId); + equal("111", postedMessage.number); + } + + do_test("readMBDN"); + do_test("readCphsMBN"); + + run_next_test(); +}); + +/** + * Verify the recovery from SimRecordHelper.readCphsMBN() if MBDN is not valid + * or is empty after SimRecordHelper.readMBDN(). + */ +add_test(function test_read_mbdn_recovered_from_cphs_mbn() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let pduHelper = context.GsmPDUHelper; + let recordHelper = context.SimRecordHelper; + let iccUtilsHelper = context.ICCUtilsHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + + io.loadLinearFixedEF = function(options) { + let mbnData = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + ]; + + // Write data size + buf.writeInt32(mbnData.length * 2); + + // Write MBN + for (let i = 0; i < mbnData.length; i++) { + pduHelper.writeHexOctet(mbnData[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(mbnData.length * 2); + + options.recordSize = mbnData.length; + if (options.callback) { + options.callback(options); + } + }; + + iccUtilsHelper.isCphsServiceAvailable = function(geckoService) { + return geckoService == "MBN"; + }; + + let isRecovered = false; + recordHelper.readCphsMBN = function(onComplete) { + isRecovered = true; + }; + + recordHelper.readMBDN(); + + equal(RIL.iccInfoPrivate.mbdn, undefined); + ok(isRecovered); + + run_next_test(); +}); + +/** + * Verify reading EF_PNN with different coding scheme. + */ +add_test(function test_pnn_with_different_coding_scheme() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let pduHelper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + let test_data = [{ + // Cell Broadcast data coding scheme - "Test1" + pnn: [0x43, 0x06, 0x85, 0xD4, 0xF2, 0x9C, 0x1E, 0x03], + expectedResult: "Test1" + },{ + // UCS2 with 0x80 - "Test1" + pnn: [0x43, 0x0C, 0x90, 0x80, 0x00, 0x54, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, 0x00, 0x31], + expectedResult: "Test1" + },{ + // UCS2 with 0x81 - "Mozilla\u694a" + pnn: [0x43, 0x0E, 0x90, 0x81, 0x08, 0xd2, 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, 0xca, 0xff, 0xff], + expectedResult: "Mozilla\u694a" + },{ + // UCS2 with 0x82 - "Mozilla\u694a" + pnn: [0x43, 0x0F, 0x90, 0x82, 0x08, 0x69, 0x00, 0x4d, 0x6f, 0x7a, 0x69, 0x6c, 0x6c, 0x61, 0xca, 0xff, 0xff], + expectedResult: "Mozilla\u694a" + }]; + + function do_test_pnn(pnn, expectedResult) { + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + // Write data size. + buf.writeInt32(pnn.length * 2); + + // Write data. + for (let i = 0; i < pnn.length; i++) { + pduHelper.writeHexOctet(pnn[i]); + } + + // Write string delimiter. + buf.writeStringDelimiter(pnn.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + record.readPNN(); + + equal(ril.iccInfoPrivate.PNN[0].fullName, expectedResult); + // Reset PNN info for next test + ril.iccInfoPrivate.PNN = null; + } + + ril.appType = CARD_APPTYPE_SIM; + for (let i = 0; i < test_data.length; i++) { + do_test_pnn(test_data[i].pnn, test_data[i].expectedResult); + } + + run_next_test(); +}); + +/** + * Verify reading EF_PNN with different content. + */ +add_test(function test_pnn_with_different_content() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let record = context.SimRecordHelper; + let pduHelper = context.GsmPDUHelper; + let ril = context.RIL; + let buf = context.Buf; + let io = context.ICCIOHelper; + + let test_data = [{ + // [0]: {"fullName":"Test1","shortName":"Test1"} + pnn: [0x43, 0x06, 0x85, 0xD4, 0xF2, 0x9C, 0x1E, 0x03, + 0x45, 0x06, 0x85, 0xD4, 0xF2, 0x9C, 0x1E, 0x03], + expectedResult: {"fullName": "Test1","shortName": "Test1"} + },{ + // [1]: {"fullName":"Test2"} + pnn: [0x43, 0x06, 0x85, 0xD4, 0xF2, 0x9C, 0x2E, 0x03], + expectedResult: {"fullName": "Test2"} + },{ + // [2]: undefined + pnn: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + },{ + // [3]: {"fullName": "Test4"} + pnn: [0x43, 0x06, 0x85, 0xD4, 0xF2, 0x9C, 0x4E, 0x03], + expectedResult: {"fullName": "Test4"} + },{ + // [4]: undefined + pnn: [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], + }]; + + function do_test_pnn() { + ril.iccIO = function fakeIccIO(options) { + let index = options.p1 - 1; + let pnn = test_data[index].pnn; + + // Write data size. + buf.writeInt32(pnn.length * 2); + + // Write data. + for (let i = 0; i < pnn.length; i++) { + pduHelper.writeHexOctet(pnn[i]); + } + + // Write string delimiter. + buf.writeStringDelimiter(pnn.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + options.p1 = 1; + options.totalRecords = test_data.length; + + ril.iccIO(options); + }; + + record.readPNN(); + + equal(test_data.length, ril.iccInfoPrivate.PNN.length); + for (let i = 0; i < test_data.length; i++) { + if (test_data[i].expectedResult) { + equal(test_data[i].expectedResult.fullName, + ril.iccInfoPrivate.PNN[i].fullName); + equal(test_data[i].expectedResult.shortName, + ril.iccInfoPrivate.PNN[i].shortName); + } else { + equal(test_data[i].expectedResult, ril.iccInfoPrivate.PNN[i]); + } + } + } + + ril.appType = CARD_APPTYPE_SIM; + do_test_pnn(); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_ruim.js b/dom/system/gonk/tests/test_ril_worker_ruim.js new file mode 100644 index 000000000..0ddc10f29 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_ruim.js @@ -0,0 +1,328 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify RUIM Service. + */ +add_test(function test_is_ruim_service_available() { + let worker = newWorker(); + let context = worker.ContextPool._contexts[0]; + context.RIL._isCdma = true; + context.RIL.appType = CARD_APPTYPE_RUIM; + + function test_table(cst, geckoService, enabled) { + context.RIL.iccInfoPrivate.cst = cst; + equal(context.ICCUtilsHelper.isICCServiceAvailable(geckoService), + enabled); + } + + test_table([0x0, 0x0, 0x0, 0x0, 0x03], "SPN", true); + test_table([0x0, 0x0, 0x0, 0x03, 0x0], "SPN", false); + test_table([0x0, 0x0C, 0x0, 0x0, 0x0], "ENHANCED_PHONEBOOK", true); + test_table([0x0, 0x0, 0x0, 0x0, 0x0], "ENHANCED_PHONEBOOK", false); + + run_next_test(); +}); + +/** + * Verify EF_PATH for RUIM file. + */ +add_test(function test_ruim_file_path_id() { + let worker = newWorker(); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let ICCFileHelper = context.ICCFileHelper; + + RIL.appType = CARD_APPTYPE_RUIM; + equal(ICCFileHelper.getEFPath(ICC_EF_CSIM_CST), + EF_PATH_MF_SIM + EF_PATH_DF_CDMA); + + run_next_test(); +}); + +add_test(function test_fetch_ruim_recodes() { + let worker = newWorker(); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let ruimHelper = context.RuimRecordHelper; + + function testFetchRuimRecordes(expectCalled) { + let ifCalled = []; + + ruimHelper.getIMSI_M = function() { + ifCalled.push("getIMSI_M"); + }; + + ruimHelper.readCST = function() { + ifCalled.push("readCST"); + }; + + ruimHelper.readCDMAHome = function() { + ifCalled.push("readCDMAHome"); + }; + + RIL.getCdmaSubscription = function() { + ifCalled.push("getCdmaSubscription"); + }; + + ruimHelper.fetchRuimRecords(); + + for (let i = 0; i < expectCalled.length; i++ ) { + if (ifCalled[i] != expectCalled[i]) { + do_print(expectCalled[i] + " is not called."); + ok(false); + } + } + } + + let expectCalled = ["getIMSI_M", "readCST", "readCDMAHome", + "getCdmaSubscription"]; + testFetchRuimRecordes(expectCalled); + + run_next_test(); +}); + +/** + * Verify RuimRecordHelper.decodeIMSIValue + */ +add_test(function test_decode_imsi_value() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + + function testDecodeImsiValue(encoded, length, expect) { + let decoded = context.RuimRecordHelper.decodeIMSIValue(encoded, length); + + equal(expect, decoded); + } + + testDecodeImsiValue( 99, 2, "00"); + testDecodeImsiValue( 90, 2, "01"); + testDecodeImsiValue( 19, 2, "20"); + testDecodeImsiValue( 23, 2, "34"); + testDecodeImsiValue(999, 3, "000"); + testDecodeImsiValue(990, 3, "001"); + testDecodeImsiValue(909, 3, "010"); + testDecodeImsiValue( 99, 3, "100"); + testDecodeImsiValue(901, 3, "012"); + testDecodeImsiValue( 19, 3, "120"); + testDecodeImsiValue( 91, 3, "102"); + testDecodeImsiValue(199, 3, "200"); + testDecodeImsiValue(123, 3, "234"); + testDecodeImsiValue(578, 3, "689"); + + run_next_test(); +}); + +/** + * Verify RuimRecordHelper.getIMSI_M + */ +add_test(function test_get_imsi_m() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + + function testDecodeImsi(encodedImsi, expectedImsi) { + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // Write data size + buf.writeInt32(encodedImsi.length * 2); + + // Write imsi + for (let i = 0; i < encodedImsi.length; i++) { + helper.writeHexOctet(encodedImsi[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(encodedImsi.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + context.RuimRecordHelper.getIMSI_M(); + let imsi = context.RIL.iccInfoPrivate.imsi; + + equal(expectedImsi, imsi) + } + + let imsi_1 = "466050081062861"; + testDecodeImsi([0x0, 0xe5, 0x03, 0xee, 0xca, 0x17, 0x5e, 0x80, 0x63, 0x01], imsi_1); + + let imsi_2 = "460038351175976"; + testDecodeImsi([0x0, 0xd4, 0x02, 0x61, 0x97, 0x01, 0x5c, 0x80, 0x67, 0x01], imsi_2); + + run_next_test(); +}); + +/** + * Verify RuimRecordHelper.readCDMAHome + */ +add_test(function test_read_cdmahome() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + + io.loadLinearFixedEF = function fakeLoadLinearFixedEF(options) { + let cdmaHome = [0xc1, 0x34, 0xff, 0xff, 0x00]; + + // Write data size + buf.writeInt32(cdmaHome.length * 2); + + // Write cdma home file. + for (let i = 0; i < cdmaHome.length; i++) { + helper.writeHexOctet(cdmaHome[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(cdmaHome.length * 2); + + // We just have 1 test record. + + options.totalRecords = 1; + if (options.callback) { + options.callback(options); + } + }; + + function testCdmaHome(expectedSystemIds, expectedNetworkIds) { + context.RuimRecordHelper.readCDMAHome(); + let cdmaHome = context.RIL.cdmaHome; + for (let i = 0; i < expectedSystemIds.length; i++) { + equal(cdmaHome.systemId[i], expectedSystemIds[i]); + equal(cdmaHome.networkId[i], expectedNetworkIds[i]); + } + equal(cdmaHome.systemId.length, expectedSystemIds.length); + equal(cdmaHome.networkId.length, expectedNetworkIds.length); + } + + testCdmaHome([13505], [65535]); + + run_next_test(); +}); + +/** + * Verify reading CDMA EF_SPN + */ +add_test(function test_read_cdmaspn() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let buf = context.Buf; + let io = context.ICCIOHelper; + + function testReadSpn(file, expectedSpn, expectedDisplayCondition) { + io.loadTransparentEF = function fakeLoadTransparentEF(options) { + // Write data size + buf.writeInt32(file.length * 2); + + // Write file. + for (let i = 0; i < file.length; i++) { + helper.writeHexOctet(file[i]); + } + + // Write string delimiter + buf.writeStringDelimiter(file.length * 2); + + if (options.callback) { + options.callback(options); + } + }; + + context.RuimRecordHelper.readSPN(); + equal(context.RIL.iccInfo.spn, expectedSpn); + equal(context.RIL.iccInfoPrivate.spnDisplayCondition, + expectedDisplayCondition); + } + + testReadSpn([0x01, 0x04, 0x06, 0x4e, 0x9e, 0x59, 0x2a, 0x96, + 0xfb, 0x4f, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff], + String.fromCharCode(0x4e9e) + + String.fromCharCode(0x592a) + + String.fromCharCode(0x96fb) + + String.fromCharCode(0x4fe1), + 0x1); + + // Test when there's no tailing 0xff in spn string. + testReadSpn([0x01, 0x04, 0x06, 0x4e, 0x9e, 0x59, 0x2a, 0x96, + 0xfb, 0x4f, 0xe1], + String.fromCharCode(0x4e9e) + + String.fromCharCode(0x592a) + + String.fromCharCode(0x96fb) + + String.fromCharCode(0x4fe1), + 0x1); + + run_next_test(); +}); + +/** + * Verify display condition for CDMA. + */ +add_test(function test_cdma_spn_display_condition() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + let context = worker.ContextPool._contexts[0]; + let RIL = context.RIL; + let ICCUtilsHelper = context.ICCUtilsHelper; + + // Set cdma. + RIL._isCdma = true; + + // Test updateDisplayCondition runs before any of SIM file is ready. + equal(ICCUtilsHelper.updateDisplayCondition(), true); + equal(RIL.iccInfo.isDisplayNetworkNameRequired, true); + equal(RIL.iccInfo.isDisplaySpnRequired, false); + + // Test with value. + function testDisplayCondition(ruimDisplayCondition, + homeSystemIds, homeNetworkIds, + currentSystemId, currentNetworkId, + expectUpdateDisplayCondition, + expectIsDisplaySPNRequired) { + RIL.iccInfoPrivate.spnDisplayCondition = ruimDisplayCondition; + RIL.cdmaHome = { + systemId: homeSystemIds, + networkId: homeNetworkIds + }; + RIL.voiceRegistrationState.cell = { + cdmaSystemId: currentSystemId, + cdmaNetworkId: currentNetworkId + }; + + equal(ICCUtilsHelper.updateDisplayCondition(), expectUpdateDisplayCondition); + equal(RIL.iccInfo.isDisplayNetworkNameRequired, false); + equal(RIL.iccInfo.isDisplaySpnRequired, expectIsDisplaySPNRequired); + }; + + // SPN is not required when ruimDisplayCondition is false. + testDisplayCondition(0x0, [123], [345], 123, 345, true, false); + + // System id and network id are all match. + testDisplayCondition(0x1, [123], [345], 123, 345, true, true); + + // Network is 65535, we should only need to match system id. + testDisplayCondition(0x1, [123], [65535], 123, 345, false, true); + + // Not match. + testDisplayCondition(0x1, [123], [456], 123, 345, true, false); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_sms.js b/dom/system/gonk/tests/test_ril_worker_sms.js new file mode 100644 index 000000000..7c1b972a7 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_sms.js @@ -0,0 +1,273 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gSmsSegmentHelper", function() { + let ns = {}; + Cu.import("resource://gre/modules/SmsSegmentHelper.jsm", ns); + return ns.SmsSegmentHelper; +}); + +const ESCAPE = "\uffff"; +const RESCTL = "\ufffe"; + +function run_test() { + run_next_test(); +} + +/** + * Verify receiving SMS-DELIVERY messages + */ + +function hexToNibble(nibble) { + nibble &= 0x0f; + if (nibble < 10) { + nibble += 48; // ASCII '0' + } else { + nibble += 55; // ASCII 'A' + } + return nibble; +} + +function pduToParcelData(pdu) { + let dataLength = 4 + pdu.length * 4 + 4; + let data = new Uint8Array(dataLength); + let offset = 0; + + // String length + data[offset++] = pdu.length & 0xFF; + data[offset++] = (pdu.length >> 8) & 0xFF; + data[offset++] = (pdu.length >> 16) & 0xFF; + data[offset++] = (pdu.length >> 24) & 0xFF; + + // PDU data + for (let i = 0; i < pdu.length; i++) { + let hi = (pdu[i] >>> 4) & 0x0F; + let lo = pdu[i] & 0x0F; + + data[offset++] = hexToNibble(hi); + data[offset++] = 0; + data[offset++] = hexToNibble(lo); + data[offset++] = 0; + } + + // String delimitor + data[offset++] = 0; + data[offset++] = 0; + data[offset++] = 0; + data[offset++] = 0; + + return data; +} + +function compose7bitPdu(lst, sst, data, septets) { + if ((lst == 0) && (sst == 0)) { + return [0x00, // SMSC + PDU_MTI_SMS_DELIVER, // firstOctet + 1, 0x00, 0, // senderAddress + 0x00, // protocolIdentifier + PDU_DCS_MSG_CODING_7BITS_ALPHABET, // dataCodingScheme + 0, 0, 0, 0, 0, 0, 0, // y m d h m s tz + septets] // userDataLength + .concat(data); + } + + return [0x00, // SMSC + PDU_MTI_SMS_DELIVER | PDU_UDHI, // firstOctet + 1, 0x00, 0, // senderAddress + 0x00, // protocolIdentifier + PDU_DCS_MSG_CODING_7BITS_ALPHABET, // dataCodingScheme + 0, 0, 0, 0, 0, 0, 0, // y m d h m s tz + 8 + septets, // userDataLength + 6, // user data header length + PDU_IEI_NATIONAL_LANGUAGE_LOCKING_SHIFT, 1, lst, // PDU_IEI_NATIONAL_LANGUAGE_LOCKING_SHIFT + PDU_IEI_NATIONAL_LANGUAGE_SINGLE_SHIFT, 1, sst] // PDU_IEI_NATIONAL_LANGUAGE_SINGLE_SHIFT + .concat(data); +} + +function composeUcs2Pdu(rawBytes) { + return [0x00, // SMSC + PDU_MTI_SMS_DELIVER, // firstOctet + 1, 0x00, 0, // senderAddress + 0x00, // protocolIdentifier + PDU_DCS_MSG_CODING_16BITS_ALPHABET, // dataCodingScheme + 0, 0, 0, 0, 0, 0, 0, // y m d h m s tz + rawBytes.length] // userDataLength + .concat(rawBytes); +} + +function newSmsParcel(pdu) { + return newIncomingParcel(-1, + RESPONSE_TYPE_UNSOLICITED, + UNSOLICITED_RESPONSE_NEW_SMS, + pduToParcelData(pdu)); +} + +function removeSpecialChar(str, needle) { + for (let i = 0; i < needle.length; i++) { + let pos; + while ((pos = str.indexOf(needle[i])) >= 0) { + str = str.substring(0, pos) + str.substring(pos + 1); + } + } + return str; +} + +function newWriteHexOctetAsUint8Worker() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + context.GsmPDUHelper.writeHexOctet = function(value) { + context.Buf.writeUint8(value); + }; + + return worker; +} + +function add_test_receiving_sms(expected, pdu) { + add_test(function test_receiving_sms() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + do_print("fullBody: " + message.fullBody); + equal(expected, message.fullBody) + } + }); + + do_print("expect: " + expected); + do_print("pdu: " + pdu); + worker.onRILMessage(0, newSmsParcel(pdu)); + + run_next_test(); + }); +} + +var test_receiving_7bit_alphabets__worker; +function test_receiving_7bit_alphabets(lst, sst) { + if (!test_receiving_7bit_alphabets__worker) { + test_receiving_7bit_alphabets__worker = newWriteHexOctetAsUint8Worker(); + } + let worker = test_receiving_7bit_alphabets__worker; + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let buf = context.Buf; + + function get7bitRawBytes(expected) { + buf.outgoingIndex = 0; + helper.writeStringAsSeptets(expected, 0, lst, sst); + + let subArray = buf.outgoingBytes.subarray(0, buf.outgoingIndex); + return Array.slice(subArray); + } + + let langTable = PDU_NL_LOCKING_SHIFT_TABLES[lst]; + let langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[sst]; + + let text = removeSpecialChar(langTable + langShiftTable, ESCAPE + RESCTL); + for (let i = 0; i < text.length;) { + let len = Math.min(70, text.length - i); + let expected = text.substring(i, i + len); + let septets = + gSmsSegmentHelper.countGsm7BitSeptets(expected, langTable, langShiftTable); + let rawBytes = get7bitRawBytes(expected); + let pdu = compose7bitPdu(lst, sst, rawBytes, septets); + add_test_receiving_sms(expected, pdu); + + i += len; + } +} + +function test_receiving_ucs2_alphabets(text) { + let worker = test_receiving_7bit_alphabets__worker; + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + + function getUCS2RawBytes(expected) { + buf.outgoingIndex = 0; + context.GsmPDUHelper.writeUCS2String(expected); + + let subArray = buf.outgoingBytes.subarray(0, buf.outgoingIndex); + return Array.slice(subArray); + } + + for (let i = 0; i < text.length;) { + let len = Math.min(70, text.length - i); + let expected = text.substring(i, i + len); + let rawBytes = getUCS2RawBytes(expected); + let pdu = composeUcs2Pdu(rawBytes); + add_test_receiving_sms(expected, pdu); + + i += len; + } +} + +var ucs2str = ""; +for (let lst = 0; lst < PDU_NL_LOCKING_SHIFT_TABLES.length; lst++) { + ucs2str += PDU_NL_LOCKING_SHIFT_TABLES[lst]; + for (let sst = 0; sst < PDU_NL_SINGLE_SHIFT_TABLES.length; sst++) { + test_receiving_7bit_alphabets(lst, sst); + + if (lst == 0) { + ucs2str += PDU_NL_SINGLE_SHIFT_TABLES[sst]; + } + } +} +test_receiving_ucs2_alphabets(ucs2str); + +// Bug 820220: B2G SMS: wrong order and truncated content in multi-part messages +add_test(function test_sendSMS_UCS2_without_langIndex_langShiftIndex_defined() { + let worker = newWriteHexOctetAsUint8Worker(); + let context = worker.ContextPool._contexts[0]; + + context.Buf.sendParcel = function() { + // Each sendParcel() call represents one outgoing segment of a multipart + // SMS message. Here, we have the first segment send, so it's "Hello " + // only. + // + // 4(parcel size) + 4(request type) + 4(token) + // + 4(two messages) + 4(null SMSC) + 4(message string length) + // + 1(first octet) + 1(message reference) + // + 2(DA len, TOA) + 4(addr) + // + 1(pid) + 1(dcs) + // + 1(UDL) + 6(UDHL, type, len, ref, max, seq) + // + 12(2 * strlen("Hello ")) + // + 4(two delimitors) = 57 + // + // If we have additional 6(type, len, langIndex, type len, langShiftIndex) + // octets here, then bug 809553 is not fixed. + equal(this.outgoingIndex, 57); + + run_next_test(); + }; + + context.RIL.sendSMS({ + number: "1", + segmentMaxSeq: 2, + fullBody: "Hello World!", + dcs: PDU_DCS_MSG_CODING_16BITS_ALPHABET, + segmentRef16Bit: false, + userDataHeaderLength: 5, + requestStatusReport: true, + segments: [ + { + body: "Hello ", + encodedBodyLength: 12, + }, { + body: "World!", + encodedBodyLength: 12, + } + ], + }); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_sms_cdma.js b/dom/system/gonk/tests/test_ril_worker_sms_cdma.js new file mode 100644 index 000000000..85d0b6e0c --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_sms_cdma.js @@ -0,0 +1,298 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/* + * Helper function to covert a HEX string to a byte array. + * + * @param hexString + * A hexadecimal string of which the length is even. + */ +function hexStringToBytes(hexString) { + let bytes = []; + + let length = hexString.length; + + for (let i = 0; i < length; i += 2) { + bytes.push(Number.parseInt(hexString.substr(i, 2), 16)); + } + + return bytes; +} + +/* + * Helper function to covert a byte array to a HEX string. + * + * @param bytes + * Could be a regular byte array or Uint8Array. + */ +function bytesToHexString(bytes) { + let hexString = ""; + let hex; + + for (let i = 0; i < bytes.length; i++) { + hex = bytes[i].toString(16).toUpperCase(); + if (hex.length === 1) { + hexString += "0"; + } + hexString += hex; + } + + return hexString; +} + +/* + * Helper function to ecode Opaque UserData + * + * @param msg_type + * PDU_CDMA_MSG_TYPE_SUBMIT or PDU_CDMA_MSG_TYPE_DELIVER + * @param data + * The byte array of opaque data to be encoded. + */ +function encodeOpaqueUserData(bitBufferHelper, options) { + let bearerDataBuffer = []; + bitBufferHelper.startWrite(bearerDataBuffer); + + // Msg-Id + bitBufferHelper.writeBits(PDU_CDMA_MSG_USERDATA_MSG_ID, 8); + bitBufferHelper.writeBits(3, 8); + bitBufferHelper.writeBits(options.msg_type, 4); // MSG_TYPE + bitBufferHelper.writeBits(1, 16); // MSG_ID + bitBufferHelper.flushWithPadding(); // HEADER_IND (1) + RESERVED (3) + + // User Data + bitBufferHelper.writeBits(PDU_CDMA_MSG_USERDATA_BODY, 8); + let dataLength = options.data.length; + bitBufferHelper.writeBits(2 + dataLength, 8); // 2 bytes for MSG_ENCODING, NUM_FIELDS + bitBufferHelper.writeBits(PDU_CDMA_MSG_CODING_OCTET, 5); //MSG_ENCODING + // MSG_TYPE is omitted if MSG_ENCODING is CODING_OCTET + bitBufferHelper.writeBits(dataLength, 8); // NUM_FIELDS + for (let i = 0; i < dataLength; i++) { // CHARi + bitBufferHelper.writeBits(options.data[i], 8); + } + bitBufferHelper.flushWithPadding(); // RESERVED (3 filling bits) + + return bearerDataBuffer; +} + +function newSmsParcel(cdmaPduHelper, pdu) { + return newIncomingParcel(-1, + RESPONSE_TYPE_UNSOLICITED, + UNSOLICITED_RESPONSE_CDMA_NEW_SMS, + pduToParcelData(cdmaPduHelper, pdu)); +} + +/* + * Helper function to encode PDU into Parcel. + * See ril_cdma_sms.h for the structure definition of RIL_CDMA_SMS_Message + * + * @param teleservice + * The Teleservice-Id of this PDU. + * See PDU_CDMA_MSG_TELESERIVCIE_ID_XXX in ril_const.js. + * @param address + * The Orginating or Destinating address. + * @param bearerData + * The byte array of the encoded bearer data. + */ +function pduToParcelData(cdmaPduHelper, pdu) { + + let addrInfo = cdmaPduHelper.encodeAddr(pdu.address); + // Teleservice, isServicePresent, ServiceCategory, + // addrInfo {digitMode, numberMode, numberType, numberPlan, address.length, address} + // Sub Address + // bearerData length, bearerData. + let dataLength = 4 + 4 + 4 + + (5 + addrInfo.address.length) * 4 + + 3 * 4 + + 4 + pdu.bearerData.length * 4; + + let data = new Uint8Array(dataLength); + let offset = 0; + + function writeInt(value) { + data[offset++] = value & 0xFF; + data[offset++] = (value >> 8) & 0xFF; + data[offset++] = (value >> 16) & 0xFF; + data[offset++] = (value >> 24) & 0xFF; + } + + function writeByte(value) { + data[offset++] = value & 0xFF; + data[offset++] = 0; + data[offset++] = 0; + data[offset++] = 0; + } + + // Teleservice + writeInt(pdu.teleservice); + + // isServicePresent + writeByte(0); + + // ServiceCategory + writeInt(PDU_CDMA_MSG_CATEGORY_UNSPEC); + + // AddrInfo + writeByte(addrInfo.digitMode); + writeByte(addrInfo.numberMode); + writeByte(addrInfo.numberType); + writeByte(addrInfo.numberPlan); + let addressLength = addrInfo.address.length; + writeByte(addressLength); + for (let i = 0; i < addressLength; i++) { + writeByte(addrInfo.address[i]); + } + + // Subaddress + writeByte(0); + writeByte(0); + writeByte(0); + + // Bearer Data Length + dataLength = pdu.bearerData.length; + writeByte(dataLength); + + // Bearer Data + for (let i = 0; i < dataLength; i++) { + writeByte(pdu.bearerData[i]); + } + + return data; +} + +/** + * Verify CDMA SMS Delivery ACK Message. + */ +add_test(function test_processCdmaSmsStatusReport() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + function test_StatusReport(errorClass, msgStatus) { + let msgId = 0; + let sentSmsMap = context.RIL._pendingSentSmsMap; + + sentSmsMap[msgId] = {}; + + let message = { + SMSC: "", + mti: 0, + udhi: 0, + sender: "0987654321", + recipient: null, + pid: PDU_PID_DEFAULT, + epid: PDU_PID_DEFAULT, + dcs: 0, + mwi: null, + replace: false, + header: null, + body: "Status: Sent, Dest: 0987654321", + data: null, + timestamp: new Date().valueOf(), + language: null, + status: null, + scts: null, + dt: null, + encoding: PDU_CDMA_MSG_CODING_7BITS_ASCII, + messageClass: GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + messageType: PDU_CDMA_MSG_TYPE_P2P, + serviceCategory: 0, + subMsgType: PDU_CDMA_MSG_TYPE_DELIVER_ACK, + msgId: msgId, + errorClass: errorClass, + msgStatus: msgStatus + }; + + context.RIL._processCdmaSmsStatusReport(message); + + let postedMessage = workerHelper.postedMessage; + + // Check if pending token is removed. + ok((errorClass === 2) ? !!sentSmsMap[msgId] : !sentSmsMap[msgId]); + + // Check the response message accordingly. + if (errorClass === -1) { + // Check if the report is treated as normal incoming SMS + equal("sms-received", postedMessage.rilMessageType); + } else if (errorClass === 2) { + // Do nothing. + } else { + // Check Delivery Status + if (errorClass === 0) { + equal(postedMessage.deliveryStatus, GECKO_SMS_DELIVERY_STATUS_SUCCESS); + } else { + equal(postedMessage.deliveryStatus, GECKO_SMS_DELIVERY_STATUS_ERROR); + } + } + } + + test_StatusReport(-1, -1); // Message Status Sub-parameter is absent. + test_StatusReport(0, 0); // 00|000000: no error|Message accepted + test_StatusReport(2, 4); // 10|000100: temporary condition|Network congestion + test_StatusReport(3, 5); // 11|000101: permanent condition|Network error + + run_next_test(); +}); + +/** + * Verify WAP Push over CDMA SMS Message. + */ +add_test(function test_processCdmaSmsWapPush() { + let workerHelper = newInterceptWorker(), + worker = workerHelper.worker, + context = worker.ContextPool._contexts[0], + bitBufferHelper = context.BitBufferHelper, + cdmaPduHelper = context.CdmaPDUHelper; + + function test_CdmaSmsWapPdu(wdpData, reversed) { + let orig_address = "0987654321", + hexString, + fullDataHexString = ""; + + for (let i = 0; i < wdpData.length; i++) { + let dataIndex = (reversed) ? (wdpData.length - i - 1) : i; + hexString = "00"; // MSG_TYPE + hexString += bytesToHexString([wdpData.length]); // TOTAL_SEG + hexString += bytesToHexString([dataIndex]); // SEG_NUM (zero-based) + if ((dataIndex === 0)) { + hexString += "23F00B84"; // SOURCE_PORT, DEST_PORT for 1st segment + } + hexString += wdpData[dataIndex]; // WDP DATA + + do_print("hexString: " + hexString); + + fullDataHexString += wdpData[i]; + + let pdu = { + teleservice: PDU_CDMA_MSG_TELESERIVCIE_ID_WAP, + address: orig_address, + bearerData: encodeOpaqueUserData(bitBufferHelper, + { msg_type: PDU_CDMA_MSG_TYPE_DELIVER, + data: hexStringToBytes(hexString) }) + }; + + worker.onRILMessage(0, newSmsParcel(cdmaPduHelper, pdu)); + } + + let postedMessage = workerHelper.postedMessage; + + do_print("fullDataHexString: " + fullDataHexString); + + equal("sms-received", postedMessage.rilMessageType); + equal(PDU_CDMA_MSG_TELESERIVCIE_ID_WAP, postedMessage.teleservice); + equal(orig_address, postedMessage.sender); + equal(0x23F0, postedMessage.header.originatorPort); + equal(0x0B84, postedMessage.header.destinationPort); + equal(fullDataHexString, bytesToHexString(postedMessage.data)); + } + + // Verify Single WAP PDU + test_CdmaSmsWapPdu(["000102030405060708090A0B0C0D0E0F"]); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_sms_cdmapduhelper.js b/dom/system/gonk/tests/test_ril_worker_sms_cdmapduhelper.js new file mode 100644 index 000000000..276728f2f --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_sms_cdmapduhelper.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify CdmaPDUHelper#encodeUserDataReplyOption. + */ +add_test(function test_CdmaPDUHelper_encodeUserDataReplyOption() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + let context = worker.ContextPool._contexts[0]; + + let testDataBuffer = []; + context.BitBufferHelper.startWrite(testDataBuffer); + + let helper = context.CdmaPDUHelper; + helper.encodeUserDataReplyOption({requestStatusReport: true}); + + let expectedDataBuffer = [PDU_CDMA_MSG_USERDATA_REPLY_OPTION, 0x01, 0x40]; + + equal(testDataBuffer.length, expectedDataBuffer.length); + + for (let i = 0; i < expectedDataBuffer.length; i++) { + equal(testDataBuffer[i], expectedDataBuffer[i]); + } + + run_next_test(); +}); + +/** + * Verify CdmaPDUHelper#cdma_decodeUserDataMsgStatus. + */ +add_test(function test_CdmaPDUHelper_decodeUserDataMsgStatus() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + let context = worker.ContextPool._contexts[0]; + + let helper = context.CdmaPDUHelper; + function test_MsgStatus(octet) { + let testDataBuffer = [octet]; + context.BitBufferHelper.startRead(testDataBuffer); + let result = helper.decodeUserDataMsgStatus(); + + equal(result.errorClass, octet >>> 6); + equal(result.msgStatus, octet & 0x3F); + } + + // 00|000000: no error|Message accepted + test_MsgStatus(0x00); + + // 10|000100: temporary condition|Network congestion + test_MsgStatus(0x84); + + // 11|000101: permanent condition|Network error + test_MsgStatus(0xC5); + + run_next_test(); +}); + +/** + * Verify CdmaPDUHelper#decodeCdmaPDUMsg. + * - encoding by shift-jis + */ +add_test(function test_CdmaPDUHelper_decodeCdmaPDUMsg_Shift_jis() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + let context = worker.ContextPool._contexts[0]; + + let helper = context.CdmaPDUHelper; + function test_decodePDUMsg(testDataBuffer, expected, encoding, msgType, msgBodySize) { + context.BitBufferHelper.startRead(testDataBuffer); + let result = helper.decodeCdmaPDUMsg(encoding, msgType, msgBodySize); + equal(result, expected); + } + + // Shift-JIS has 1 byte and 2 byte code for one character and has some types of characters: + // Hiragana, Kanji, Katakana(fullwidth, halfwidth)... + // This test is a combination of 1 byte and 2 byte code and types of characters. + + // test case 1 + let testDataBuffer1 = [0x82, 0x58, 0x33, 0x41, 0x61, 0x33, 0x82, 0x60, + 0x82, 0x81, 0x33, 0xB1, 0xAF, 0x33, 0x83, 0x41, + 0x83, 0x96, 0x33, 0x82, 0xA0, 0x33, 0x93, 0xFA, + 0x33, 0x3A, 0x3C, 0x33, 0x81, 0x80, 0x81, 0x8E, + 0x33, 0x31, 0x82, 0x51, 0x41, 0x61, 0x82, 0x51, + 0x82, 0x60, 0x82, 0x81, 0x82, 0x51, 0xB1, 0xAF, + 0x82, 0x51, 0x83, 0x41, 0x83, 0x96, 0x82, 0x51, + 0x82, 0xA0, 0x82, 0x51, 0x93, 0xFA, 0x82, 0x51, + 0x3A, 0x3C, 0x82, 0x51, 0x81, 0x80, 0x81, 0x8E, + 0x82, 0x51]; + + test_decodePDUMsg( + testDataBuffer1, + "\uFF19\u0033\u0041\u0061\u0033\uFF21\uFF41\u0033\uFF71\uFF6F\u0033\u30A2\u30F6\u0033\u3042\u0033\u65E5\u0033\u003A\u003C\u0033\u00F7\u2103\u0033\u0031\uFF12\u0041\u0061\uFF12\uFF21\uFF41\uFF12\uFF71\uFF6F\uFF12\u30A2\u30F6\uFF12\u3042\uFF12\u65E5\uFF12\u003A\u003C\uFF12\u00F7\u2103\uFF12", + PDU_CDMA_MSG_CODING_SHIFT_JIS, + undefined, + testDataBuffer1.length + ); + + // test case 2 + let testDataBuffer2 = [0x31, 0x51, 0x63, 0x82, 0x58, 0x51, 0x63, 0x82, + 0x60, 0x82, 0x81, 0x51, 0x63, 0xB1, 0xAF, 0x51, + 0x63, 0x83, 0x41, 0x83, 0x96, 0x51, 0x63, 0x82, + 0xA0, 0x51, 0x63, 0x93, 0xFA, 0x51, 0x63, 0x3A, + 0x3C, 0x51, 0x63, 0x81, 0x80, 0x81, 0x8E, 0x51, + 0x63, 0x31, 0x82, 0x70, 0x82, 0x85, 0x82, 0x58, + 0x82, 0x70, 0x82, 0x85, 0x41, 0x61, 0x82, 0x70, + 0x82, 0x85, 0xB1, 0xAF, 0x82, 0x70, 0x82, 0x85, + 0x83, 0x41, 0x83, 0x96, 0x82, 0x70, 0x82, 0x85, + 0x82, 0xA0, 0x82, 0x70, 0x82, 0x85, 0x93, 0xFA, + 0x82, 0x70, 0x82, 0x85, 0x3A, 0x3C, 0x82, 0x70, + 0x82, 0x85, 0x81, 0x80, 0x81, 0x8E, 0x82, 0x70, + 0x82, 0x85]; + + test_decodePDUMsg( + testDataBuffer2, + "\u0031\u0051\u0063\uFF19\u0051\u0063\uFF21\uFF41\u0051\u0063\uFF71\uFF6F\u0051\u0063\u30A2\u30F6\u0051\u0063\u3042\u0051\u0063\u65E5\u0051\u0063\u003A\u003C\u0051\u0063\u00F7\u2103\u0051\u0063\u0031\uFF31\uFF45\uFF19\uFF31\uFF45\u0041\u0061\uFF31\uFF45\uFF71\uFF6F\uFF31\uFF45\u30A2\u30F6\uFF31\uFF45\u3042\uFF31\uFF45\u65E5\uFF31\uFF45\u003A\u003C\uFF31\uFF45\u00F7\u2103\uFF31\uFF45", + PDU_CDMA_MSG_CODING_SHIFT_JIS, + undefined, + testDataBuffer2.length + ); + + // test case 3 + let testDataBuffer3 = [0x31, 0xC2, 0xDF, 0x82, 0x58, 0xC2, 0xDF, 0x41, + 0x61, 0xC2, 0xDF, 0x82, 0x60, 0x82, 0x81, 0xC2, + 0xDF, 0x83, 0x41, 0x83, 0x96, 0xC2, 0xDF, 0x82, + 0xA0, 0xC2, 0xDF, 0x93, 0xFA, 0xC2, 0xDF, 0x3A, + 0x3C, 0xC2, 0xDF, 0x81, 0x80, 0x81, 0x8E, 0xC2, + 0xDF, 0x31, 0x83, 0x51, 0x83, 0x87, 0x82, 0x58, + 0x83, 0x51, 0x83, 0x87, 0x41, 0x61, 0x83, 0x51, + 0x83, 0x87, 0x82, 0x60, 0x82, 0x81, 0x83, 0x51, + 0x83, 0x87, 0xB1, 0xAF, 0x83, 0x51, 0x83, 0x87, + 0x82, 0xA0, 0x83, 0x51, 0x83, 0x87, 0x93, 0xFA, + 0x83, 0x51, 0x83, 0x87, 0x3A, 0x3C, 0x83, 0x51, + 0x83, 0x87, 0x81, 0x80, 0x81, 0x8E, 0x83, 0x51, + 0x83, 0x87]; + + test_decodePDUMsg( + testDataBuffer3, + "\u0031\uFF82\uFF9F\uFF19\uFF82\uFF9F\u0041\u0061\uFF82\uFF9F\uFF21\uFF41\uFF82\uFF9F\u30A2\u30F6\uFF82\uFF9F\u3042\uFF82\uFF9F\u65E5\uFF82\uFF9F\u003A\u003C\uFF82\uFF9F\u00F7\u2103\uFF82\uFF9F\u0031\u30B2\u30E7\uFF19\u30B2\u30E7\u0041\u0061\u30B2\u30E7\uFF21\uFF41\u30B2\u30E7\uFF71\uFF6F\u30B2\u30E7\u3042\u30B2\u30E7\u65E5\u30B2\u30E7\u003A\u003C\u30B2\u30E7\u00F7\u2103\u30B2\u30E7", + PDU_CDMA_MSG_CODING_SHIFT_JIS, + undefined, + testDataBuffer3.length + ); + + // test case 4 + let testDataBuffer4 = [0x31, 0x82, 0xB0, 0x82, 0x58, 0x82, 0xB0, 0x41, + 0x61, 0x82, 0xB0, 0x82, 0x60, 0x82, 0x81, 0x82, + 0xB0, 0xB1, 0xAF, 0x82, 0xB0, 0x83, 0x41, 0x83, + 0x96, 0x82, 0xB0, 0x93, 0xFA, 0x82, 0xB0, 0x3A, + 0x3C, 0x82, 0xB0, 0x81, 0x80, 0x81, 0x8E, 0x82, + 0xB0, 0x31, 0x88, 0xA4, 0x82, 0x58, 0x88, 0xA4, + 0x41, 0x61, 0x88, 0xA4, 0x82, 0x60, 0x82, 0x81, + 0x88, 0xA4, 0xB1, 0xAF, 0x88, 0xA4, 0x83, 0x41, + 0x83, 0x96, 0x88, 0xA4, 0x82, 0xA0, 0x88, 0xA4, + 0x3A, 0x3C, 0x88, 0xA4, 0x81, 0x80, 0x81, 0x8E, + 0x88, 0xA4]; + + test_decodePDUMsg( + testDataBuffer4, + "\u0031\u3052\uFF19\u3052\u0041\u0061\u3052\uFF21\uFF41\u3052\uFF71\uFF6F\u3052\u30A2\u30F6\u3052\u65E5\u3052\u003A\u003C\u3052\u00F7\u2103\u3052\u0031\u611B\uFF19\u611B\u0041\u0061\u611B\uFF21\uFF41\u611B\uFF71\uFF6F\u611B\u30A2\u30F6\u611B\u3042\u611B\u003A\u003C\u611B\u00F7\u2103\u611B", + PDU_CDMA_MSG_CODING_SHIFT_JIS, + undefined, + testDataBuffer4.length + ); + + // test case 5 + let testDataBuffer5 = [0x31, 0x40, 0x82, 0x58, 0x40, 0x41, 0x61, 0x40, + 0x82, 0x60, 0x82, 0x81, 0x40, 0xB1, 0xAF, 0x40, + 0x83, 0x41, 0x83, 0x96, 0x40, 0x82, 0xA0, 0x40, + 0x93, 0xFA, 0x40, 0x81, 0x80, 0x81, 0x8E, 0x40, + 0x31, 0x81, 0x9B, 0x82, 0x58, 0x81, 0x9B, 0x41, + 0x61, 0x81, 0x9B, 0x82, 0x60, 0x82, 0x81, 0x81, + 0x9B, 0xB1, 0xAF, 0x81, 0x9B, 0x83, 0x41, 0x83, + 0x96, 0x81, 0x9B, 0x82, 0xA0, 0x81, 0x9B, 0x93, + 0xFA, 0x81, 0x9B, 0x3A, 0x3C, 0x81, 0x9B]; + + test_decodePDUMsg( + testDataBuffer5, + "\u0031\u0040\uFF19\u0040\u0041\u0061\u0040\uFF21\uFF41\u0040\uFF71\uFF6F\u0040\u30A2\u30F6\u0040\u3042\u0040\u65E5\u0040\u00F7\u2103\u0040\u0031\u25CB\uFF19\u25CB\u0041\u0061\u25CB\uFF21\uFF41\u25CB\uFF71\uFF6F\u25CB\u30A2\u30F6\u25CB\u3042\u25CB\u65E5\u25CB\u003A\u003C\u25CB", + PDU_CDMA_MSG_CODING_SHIFT_JIS, + undefined, + testDataBuffer5.length + ); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_sms_gsmpduhelper.js b/dom/system/gonk/tests/test_ril_worker_sms_gsmpduhelper.js new file mode 100644 index 000000000..f52c64cf8 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_sms_gsmpduhelper.js @@ -0,0 +1,282 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Verify GsmPDUHelper#readDataCodingScheme. + */ +add_test(function test_GsmPDUHelper_readDataCodingScheme() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + function test_dcs(dcs, encoding, messageClass, mwi) { + helper.readHexOctet = function() { + return dcs; + } + + let msg = {}; + helper.readDataCodingScheme(msg); + + equal(msg.dcs, dcs); + equal(msg.encoding, encoding); + equal(msg.messageClass, messageClass); + equal(msg.mwi == null, mwi == null); + if (mwi != null) { + equal(msg.mwi.active, mwi.active); + equal(msg.mwi.discard, mwi.discard); + equal(msg.mwi.msgCount, mwi.msgCount); + } + } + + // Group 00xx + // Bit 3 and 2 indicate the character set being used. + test_dcs(0x00, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + test_dcs(0x04, PDU_DCS_MSG_CODING_8BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + test_dcs(0x08, PDU_DCS_MSG_CODING_16BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + test_dcs(0x0C, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + // Bit 4, if set to 0, indicates that bits 1 to 0 are reserved and have no + // message class meaning. + test_dcs(0x01, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + test_dcs(0x02, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + test_dcs(0x03, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + // Bit 4, if set to 1, indicates that bits 1 to 0 have a message class meaning. + test_dcs(0x10, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]); + test_dcs(0x11, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_1]); + test_dcs(0x12, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_2]); + test_dcs(0x13, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_3]); + + // Group 01xx + test_dcs(0x50, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]); + + // Group 1000..1011: reserved + test_dcs(0x8F, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + test_dcs(0x9F, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + test_dcs(0xAF, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + test_dcs(0xBF, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL]); + + // Group 1100: Message Waiting Indication Group: Discard Message + // Bit 3 indicates Indication Sense: + test_dcs(0xC0, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + {active: false, discard: true, msgCount: 0}); + test_dcs(0xC8, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + {active: true, discard: true, msgCount: -1}); + // Bit 2 is reserved, and set to 0: + test_dcs(0xCC, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + {active: true, discard: true, msgCount: -1}); + + // Group 1101: Message Waiting Indication Group: Store Message + // Bit 3 indicates Indication Sense: + test_dcs(0xD0, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + {active: false, discard: false, msgCount: 0}); + test_dcs(0xD8, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + {active: true, discard: false, msgCount: -1}); + // Bit 2 is reserved, and set to 0: + test_dcs(0xDC, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + {active: true, discard: false, msgCount: -1}); + + // Group 1110: Message Waiting Indication Group: Store Message, UCS2 + // Bit 3 indicates Indication Sense: + test_dcs(0xE0, PDU_DCS_MSG_CODING_16BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + {active: false, discard: false, msgCount: 0}); + test_dcs(0xE8, PDU_DCS_MSG_CODING_16BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + {active: true, discard: false, msgCount: -1}); + // Bit 2 is reserved, and set to 0: + test_dcs(0xEC, PDU_DCS_MSG_CODING_16BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_NORMAL], + {active: true, discard: false, msgCount: -1}); + + // Group 1111 + test_dcs(0xF0, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]); + test_dcs(0xF1, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_1]); + test_dcs(0xF2, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_2]); + test_dcs(0xF3, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_3]); + test_dcs(0xF4, PDU_DCS_MSG_CODING_8BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]); + test_dcs(0xF5, PDU_DCS_MSG_CODING_8BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_1]); + test_dcs(0xF6, PDU_DCS_MSG_CODING_8BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_2]); + test_dcs(0xF7, PDU_DCS_MSG_CODING_8BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_3]); + // Bit 3 is reserved and should be set to 0, but if it doesn't we should + // ignore it. + test_dcs(0xF8, PDU_DCS_MSG_CODING_7BITS_ALPHABET, + GECKO_SMS_MESSAGE_CLASSES[PDU_DCS_MSG_CLASS_0]); + + run_next_test(); +}); + +/** + * Verify GsmPDUHelper#writeStringAsSeptets() padding bits handling. + */ +add_test(function test_GsmPDUHelper_writeStringAsSeptets() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + helper.resetOctetWritten = function() { + helper.octetsWritten = 0; + }; + helper.writeHexOctet = function() { + helper.octetsWritten++; + }; + + let base = "AAAAAAAA"; // Base string of 8 characters long + for (let len = 0; len < 8; len++) { + let str = base.substring(0, len); + + for (let paddingBits = 0; paddingBits < 8; paddingBits++) { + do_print("Verifying GsmPDUHelper.writeStringAsSeptets(" + + str + ", " + paddingBits + ", <default>, <default>)"); + helper.resetOctetWritten(); + helper.writeStringAsSeptets(str, paddingBits, PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT); + equal(Math.ceil(((len * 7) + paddingBits) / 8), + helper.octetsWritten); + } + } + + run_next_test(); +}); + +/** + * Verify that encoding with Spanish locking shift table generates the same + * septets as with GSM default alphabet table. + * + * Bug 1138841 - Incorrect Spanish national language locking shift table + * definition. + */ +add_test(function test_GsmPDUHelper_writeStringAsSeptets_spanish_fallback() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + let buf = []; + helper.writeHexOctet = function(octet) { + buf.push(octet); + } + + // Simple message string which is covered by GSM default alphabet. + let msg = "The quick brown fox jumps over the lazy dog"; + + // Encoded with GSM default alphabet. + helper.writeStringAsSeptets(msg, 0 /* paddingBits */, + PDU_NL_IDENTIFIER_DEFAULT, PDU_NL_IDENTIFIER_DEFAULT); + let octetsWithDefaultTable = buf; + buf = []; + + // Encoded with Spanish locking shift table. + helper.writeStringAsSeptets(msg, 0 /* paddingBits */, + PDU_NL_IDENTIFIER_SPANISH, PDU_NL_IDENTIFIER_SPANISH); + + // The length and content should be equal to what encoded with GSM default + // alphabet. + equal(octetsWithDefaultTable.length, buf.length); + for (let i = 0; i < buf.length; i++) { + equal(octetsWithDefaultTable[i], buf[i]); + } + + run_next_test(); +}); + +/** + * Verify GsmPDUHelper#readAddress + */ +add_test(function test_GsmPDUHelper_readAddress() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + function test_address(addrHex, addrString) { + let uint16Array = []; + let ix = 0; + for (let i = 0; i < addrHex.length; ++i) { + uint16Array[i] = addrHex[i].charCodeAt(); + } + + context.Buf.readUint16 = function(){ + if(ix >= uint16Array.length) { + do_throw("out of range in uint16Array"); + } + return uint16Array[ix++]; + } + let length = helper.readHexOctet(); + let parsedAddr = helper.readAddress(length); + equal(parsedAddr, addrString); + } + + // For AlphaNumeric + test_address("04D01100", "_@"); + test_address("04D01000", "\u0394@"); + + // Direct prepand + test_address("0B914151245584F6", "+14154255486"); + test_address("0E914151245584B633", "+14154255486#33"); + + // PDU_TOA_NATIONAL + test_address("0BA14151245584F6", "14154255486"); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_sms_nl_tables.js b/dom/system/gonk/tests/test_ril_worker_sms_nl_tables.js new file mode 100644 index 000000000..32bc5dc2a --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_sms_nl_tables.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +const ESCAPE = "\uffff"; +const RESCTL = "\ufffe"; +const LF = "\n"; +const CR = "\r"; +const SP = " "; +const FF = "\u000c"; + +function run_test() { + run_next_test(); +} + +/** + * Verify validity of the national language tables + */ +add_test(function test_nl_locking_shift_tables_validity() { + for (let lst = 0; lst < PDU_NL_LOCKING_SHIFT_TABLES.length; lst++) { + do_print("Verifying PDU_NL_LOCKING_SHIFT_TABLES[" + lst + "]"); + + let table = PDU_NL_LOCKING_SHIFT_TABLES[lst]; + + // Make sure table length is 128, or it will break table lookup algorithm. + equal(table.length, 128); + + // Make sure special values are preserved. + equal(table[PDU_NL_EXTENDED_ESCAPE], ESCAPE); + equal(table[PDU_NL_LINE_FEED], LF); + equal(table[PDU_NL_CARRIAGE_RETURN], CR); + equal(table[PDU_NL_SPACE], SP); + } + + run_next_test(); +}); + +add_test(function test_nl_single_shift_tables_validity() { + for (let sst = 0; sst < PDU_NL_SINGLE_SHIFT_TABLES.length; sst++) { + do_print("Verifying PDU_NL_SINGLE_SHIFT_TABLES[" + sst + "]"); + + let table = PDU_NL_SINGLE_SHIFT_TABLES[sst]; + + // Make sure table length is 128, or it will break table lookup algorithm. + equal(table.length, 128); + + // Make sure special values are preserved. + equal(table[PDU_NL_EXTENDED_ESCAPE], ESCAPE); + equal(table[PDU_NL_PAGE_BREAK], FF); + equal(table[PDU_NL_RESERVED_CONTROL], RESCTL); + } + + run_next_test(); +}); + +add_test(function test_gsm_sms_strict_7bit_charmap_validity() { + let defaultTable = PDU_NL_LOCKING_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + let defaultShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[PDU_NL_IDENTIFIER_DEFAULT]; + for (let from in GSM_SMS_STRICT_7BIT_CHARMAP) { + let to = GSM_SMS_STRICT_7BIT_CHARMAP[from]; + do_print("Verifying GSM_SMS_STRICT_7BIT_CHARMAP[\"\\u0x" + + from.charCodeAt(0).toString(16) + "\"] => \"\\u" + + to.charCodeAt(0).toString(16) + "\""); + + // Make sure "from" is not in default table + equal(defaultTable.indexOf(from), -1); + equal(defaultShiftTable.indexOf(from), -1); + // Make sure "to" is in default table + if ((defaultTable.indexOf(to) < 0) + && (defaultShiftTable.indexOf(to) < 0)) { + equal(false, true); + } + } + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_sms_segment_info.js b/dom/system/gonk/tests/test_ril_worker_sms_segment_info.js new file mode 100644 index 000000000..2b29ac60e --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_sms_segment_info.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gSmsSegmentHelper", function() { + let ns = {}; + Cu.import("resource://gre/modules/SmsSegmentHelper.jsm", ns); + return ns.SmsSegmentHelper; +}); + +const ESCAPE = "\uffff"; +const RESCTL = "\ufffe"; + +function run_test() { + run_next_test(); +} + +/** + * Verify SmsSegmentHelper#countGsm7BitSeptets() and + * GsmPDUHelper#writeStringAsSeptets() algorithm match each other. + */ +add_test(function test_SmsSegmentHelper__countGsm7BitSeptets() { + let worker = newWorker({ + postRILMessage: function(data) { + // Do nothing + }, + postMessage: function(message) { + // Do nothing + } + }); + + let context = worker.ContextPool._contexts[0]; + let helper = context.GsmPDUHelper; + helper.resetOctetWritten = function() { + helper.octetsWritten = 0; + }; + helper.writeHexOctet = function() { + helper.octetsWritten++; + }; + + function do_check_calc(str, expectedCalcLen, lst, sst, strict7BitEncoding, strToWrite) { + equal(expectedCalcLen, + gSmsSegmentHelper + .countGsm7BitSeptets(str, + PDU_NL_LOCKING_SHIFT_TABLES[lst], + PDU_NL_SINGLE_SHIFT_TABLES[sst], + strict7BitEncoding)); + + helper.resetOctetWritten(); + strToWrite = strToWrite || str; + helper.writeStringAsSeptets(strToWrite, 0, lst, sst); + equal(Math.ceil(expectedCalcLen * 7 / 8), helper.octetsWritten); + } + + // Test calculation encoded message length using both locking/single shift tables. + for (let lst = 0; lst < PDU_NL_LOCKING_SHIFT_TABLES.length; lst++) { + let langTable = PDU_NL_LOCKING_SHIFT_TABLES[lst]; + + let str = langTable.substring(0, PDU_NL_EXTENDED_ESCAPE) + + langTable.substring(PDU_NL_EXTENDED_ESCAPE + 1); + + for (let sst = 0; sst < PDU_NL_SINGLE_SHIFT_TABLES.length; sst++) { + let langShiftTable = PDU_NL_SINGLE_SHIFT_TABLES[sst]; + + // <escape>, <resctrl> should be ignored. + do_check_calc(ESCAPE + RESCTL, 0, lst, sst); + + // Characters defined in locking shift table should be encoded directly. + do_check_calc(str, str.length, lst, sst); + + let [str1, str2] = ["", ""]; + for (let i = 0; i < langShiftTable.length; i++) { + if ((i == PDU_NL_EXTENDED_ESCAPE) || (i == PDU_NL_RESERVED_CONTROL)) { + continue; + } + + let c = langShiftTable[i]; + if (langTable.indexOf(c) >= 0) { + str1 += c; + } else { + str2 += c; + } + } + + // Characters found in both locking/single shift tables should be + // directly encoded. + do_check_calc(str1, str1.length, lst, sst); + + // Characters found only in single shift tables should be encoded as + // <escape><code>, therefore doubles its original length. + do_check_calc(str2, str2.length * 2, lst, sst); + } + } + + // Bug 790192: support strict GSM SMS 7-Bit encoding + let str = "", strToWrite = "", gsmLen = 0; + for (let c in GSM_SMS_STRICT_7BIT_CHARMAP) { + str += c; + strToWrite += GSM_SMS_STRICT_7BIT_CHARMAP[c]; + if (PDU_NL_LOCKING_SHIFT_TABLES.indexOf(GSM_SMS_STRICT_7BIT_CHARMAP[c])) { + gsmLen += 1; + } else { + gsmLen += 2; + } + } + do_check_calc(str, gsmLen, + PDU_NL_IDENTIFIER_DEFAULT, PDU_NL_IDENTIFIER_DEFAULT, + true, strToWrite); + + run_next_test(); +}); + diff --git a/dom/system/gonk/tests/test_ril_worker_smsc_address.js b/dom/system/gonk/tests/test_ril_worker_smsc_address.js new file mode 100644 index 000000000..c8c283b7c --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_smsc_address.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +const SMSC_ATT = '+13123149810'; +const SMSC_ATT_TYPO = '+++1312@@@314$$$9,8,1,0'; +const SMSC_ATT_TEXT = '"+13123149810",145'; +const SMSC_ATT_TEXT_INCORRECT_TOA = '"+13123149810",129'; +const SMSC_ATT_PDU = '07913121139418F0'; +const SMSC_O2 = '+447802000332'; +const SMSC_O2_TEXT = '"+447802000332",145'; +const SMSC_O2_PDU = '0791448720003023'; +const SMSC_EMPTY = ''; +const SMSC_TON_UNKNOWN = '0407485455' +const SMSC_TON_UNKNOWN_TEXT = '"0407485455",129'; +const SMSC_TON_UNKNOWN_TEXT_NO_TOA = '"0407485455"'; +const SMSC_TON_UNKNOWN_TEXT_INVALID_TOA = '"0407485455",abc'; +const SMSC_TON_UNKNOWN_PDU = '06814070844555'; +const SMSC_EMPTY_PDU = 'FFFFFFFFFFFFFFFFFFFFFFFF'; +const SMSC_EMPTY_TEXT = ''; + +function run_test() { + run_next_test(); +} + +function setSmsc(context, smsc, ton, npi, expected) { + context.Buf.postRILMessage = function() { + equal(this.readString(), expected); + }; + + context.RIL.setSmscAddress({ + smscAddress: smsc, + typeOfNumber: ton, + numberPlanIdentification: npi + }); +} + +function getSmsc(worker, context, raw, smsc, ton, npi) { + worker.postMessage = function(message) { + equal(message.smscAddress, smsc); + equal(message.typeOfNumber, ton); + equal(message.numberPlanIdentification, npi); + } + + context.Buf.writeString(raw); + context.RIL[REQUEST_GET_SMSC_ADDRESS](0, { rilMessageType: "getSmscAddress"}); +} + +add_test(function test_setSmscAddress() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let parcelTypes = []; + context.Buf.newParcel = (type, options) => parcelTypes.push(type); + + // Test text mode. + worker.RILQUIRKS_SMSC_ADDRESS_FORMAT = "text"; + + setSmsc(context, SMSC_ATT, 1, 1, SMSC_ATT_TEXT); + equal(parcelTypes.pop(), REQUEST_SET_SMSC_ADDRESS); + + setSmsc(context, SMSC_O2, 1, 1, SMSC_O2_TEXT); + equal(parcelTypes.pop(), REQUEST_SET_SMSC_ADDRESS); + + setSmsc(context, SMSC_ATT_TYPO, 1, 1, SMSC_ATT_TEXT); + equal(parcelTypes.pop(), REQUEST_SET_SMSC_ADDRESS); + + setSmsc(context, SMSC_TON_UNKNOWN, 0, 1, SMSC_TON_UNKNOWN_TEXT); + equal(parcelTypes.pop(), REQUEST_SET_SMSC_ADDRESS); + + // Test pdu mode. + worker.RILQUIRKS_SMSC_ADDRESS_FORMAT = "pdu"; + + setSmsc(context, SMSC_ATT, 1, 1, SMSC_ATT_PDU); + equal(parcelTypes.pop(), REQUEST_SET_SMSC_ADDRESS); + + setSmsc(context, SMSC_O2, 1, 1, SMSC_O2_PDU); + equal(parcelTypes.pop(), REQUEST_SET_SMSC_ADDRESS); + + setSmsc(context, SMSC_ATT_TYPO, 1, 1, SMSC_ATT_PDU); + equal(parcelTypes.pop(), REQUEST_SET_SMSC_ADDRESS); + + setSmsc(context, SMSC_TON_UNKNOWN, 0, 1, SMSC_TON_UNKNOWN_PDU); + equal(parcelTypes.pop(), REQUEST_SET_SMSC_ADDRESS); + + run_next_test(); +}); + +add_test(function test_getSmscAddress() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + + // Test text mode. + worker.RILQUIRKS_SMSC_ADDRESS_FORMAT = "text"; + getSmsc(worker, context, SMSC_ATT_TEXT, SMSC_ATT, 1, 1); + getSmsc(worker, context, SMSC_ATT_TEXT_INCORRECT_TOA, SMSC_ATT, 1, 1); + getSmsc(worker, context, SMSC_O2_TEXT, SMSC_O2, 1, 1); + getSmsc(worker, context, SMSC_TON_UNKNOWN_TEXT, SMSC_TON_UNKNOWN, 0, 1); + getSmsc(worker, context, SMSC_TON_UNKNOWN_TEXT_NO_TOA, SMSC_TON_UNKNOWN, 0, 1); + getSmsc(worker, context, SMSC_TON_UNKNOWN_TEXT_INVALID_TOA, SMSC_TON_UNKNOWN, + 0, 1); + getSmsc(worker, context, SMSC_EMPTY_TEXT, SMSC_EMPTY, 0, 1); + + // Test pdu mode. + worker.RILQUIRKS_SMSC_ADDRESS_FORMAT = "pdu"; + getSmsc(worker, context, SMSC_ATT_PDU, SMSC_ATT, 1, 1); + getSmsc(worker, context, SMSC_O2_PDU, SMSC_O2, 1, 1); + getSmsc(worker, context, SMSC_TON_UNKNOWN_PDU, SMSC_TON_UNKNOWN, 0, 1); + getSmsc(worker, context, SMSC_EMPTY_PDU, SMSC_EMPTY, 0, 1); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_ssn.js b/dom/system/gonk/tests/test_ril_worker_ssn.js new file mode 100644 index 000000000..ea0a2a599 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_ssn.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +add_test(function test_notification() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + function Call(callIndex, number) { + this.callIndex = callIndex; + this.number = number; + } + + Call.prototype = { + // Should use CALL_STATE_ACTIVE. + // Any new outgoing call (state = dialing or alerting) will be drop if there + // is no pending outgoing call created before. + state: CALL_STATE_ACTIVE, + //callIndex: 0, + toa: 0, + isMpty: false, + isMT: false, + als: 0, + isVoice: true, + isVoicePrivacy: false, + //number: null, + numberPresentation: 0, + name: null, + namePresentation: 0, + uusInfo: null + }; + + let oneCall = { + 0: new Call(0, '00000') + }; + + let twoCalls = { + 0: new Call(0, '00000'), + 1: new Call(1, '11111') + }; + + function testNotification(calls, code, number, resultNotification) { + + let testInfo = {calls: calls, code: code, number: number, + resultNotification: resultNotification}; + do_print('Test case info: ' + JSON.stringify(testInfo)); + + // Set current calls. + context.RIL.sendChromeMessage({ + rilMessageType: "currentCalls", + calls: calls + }); + + let notificationInfo = { + notificationType: 1, // MT + code: code, + index: 0, + type: 0, + number: number + }; + + context.RIL._processSuppSvcNotification(notificationInfo); + + let postedMessage = workerHelper.postedMessage; + equal(postedMessage.rilMessageType, 'suppSvcNotification'); + equal(postedMessage.number, number); + equal(postedMessage.notification, resultNotification); + + // Clear all existed calls. + context.RIL.sendChromeMessage({ + rilMessageType: "currentCalls", + calls: {} + }); + } + + testNotification(oneCall, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, null, + GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD); + + testNotification(oneCall, SUPP_SVC_NOTIFICATION_CODE2_RETRIEVED, null, + GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED); + + testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, null, + GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD); + + testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_RETRIEVED, null, + GECKO_SUPP_SVC_NOTIFICATION_REMOTE_RESUMED); + + testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, '00000', + GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD); + + testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, '11111', + GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD); + + testNotification(twoCalls, SUPP_SVC_NOTIFICATION_CODE2_PUT_ON_HOLD, '22222', + GECKO_SUPP_SVC_NOTIFICATION_REMOTE_HELD); + + run_next_test(); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_stk.js b/dom/system/gonk/tests/test_ril_worker_stk.js new file mode 100644 index 000000000..49b914e89 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_stk.js @@ -0,0 +1,1698 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +/** + * Helper function. + */ +function newUint8SupportOutgoingIndexWorker() { + let worker = newWorker(); + let index = 4; // index for read + let buf = [0, 0, 0, 0]; // Preserved parcel size + let context = worker.ContextPool._contexts[0]; + + context.Buf.writeUint8 = function(value) { + if (context.Buf.outgoingIndex >= buf.length) { + buf.push(value); + } else { + buf[context.Buf.outgoingIndex] = value; + } + + context.Buf.outgoingIndex++; + }; + + context.Buf.readUint8 = function() { + return buf[index++]; + }; + + context.Buf.seekIncoming = function(offset) { + index += offset; + }; + + worker.debug = do_print; + + return worker; +} + +// Test RIL requests related to STK. +/** + * Verify if RIL.sendStkTerminalProfile be called. + */ +add_test(function test_if_send_stk_terminal_profile() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let profileSend = false; + context.RIL.sendStkTerminalProfile = function(data) { + profileSend = true; + }; + + let iccStatus = { + gsmUmtsSubscriptionAppIndex: 0, + apps: [{ + app_state: CARD_APPSTATE_READY, + app_type: CARD_APPTYPE_USIM + }], + }; + worker.RILQUIRKS_SEND_STK_PROFILE_DOWNLOAD = false; + + context.RIL._processICCStatus(iccStatus); + + equal(profileSend, false); + + run_next_test(); +}); + +/** + * Verify RIL.sendStkTerminalProfile + */ +add_test(function test_send_stk_terminal_profile() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let ril = context.RIL; + let buf = context.Buf; + + ril.sendStkTerminalProfile(STK_SUPPORTED_TERMINAL_PROFILE); + + buf.seekIncoming(8); + let profile = buf.readString(); + for (let i = 0; i < STK_SUPPORTED_TERMINAL_PROFILE.length; i++) { + equal(parseInt(profile.substring(2 * i, 2 * i + 2), 16), + STK_SUPPORTED_TERMINAL_PROFILE[i]); + } + + run_next_test(); +}); + +/** + * Verify STK terminal response + */ +add_test(function test_stk_terminal_response() { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_TERMINAL_RESPONSE); + + // Token : we don't care + this.readInt32(); + + // Data Size, 44 = 2 * (TLV_COMMAND_DETAILS_SIZE(5) + + // TLV_DEVICE_ID_SIZE(4) + + // TLV_RESULT_SIZE(3) + + // TEXT LENGTH(10)) + equal(this.readInt32(), 44); + + // Command Details, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_COMMAND_DETAILS | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 3); + equal(pduHelper.readHexOctet(), 0x01); + equal(pduHelper.readHexOctet(), STK_CMD_PROVIDE_LOCAL_INFO); + equal(pduHelper.readHexOctet(), STK_LOCAL_INFO_NNA); + + // Device Identifies, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + // Result + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_RESULT | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_RESULT_OK); + + // Text + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_TEXT_STRING | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 8); + equal(pduHelper.readHexOctet(), STK_TEXT_CODING_GSM_7BIT_PACKED); + equal(pduHelper.readSeptetsToString(7, 0, PDU_NL_IDENTIFIER_DEFAULT, + PDU_NL_IDENTIFIER_DEFAULT), "Mozilla"); + + run_next_test(); + }; + + let response = { + command: { + commandNumber: 0x01, + typeOfCommand: STK_CMD_PROVIDE_LOCAL_INFO, + commandQualifier: STK_LOCAL_INFO_NNA, + options: { + isPacked: true + } + }, + input: "Mozilla", + resultCode: STK_RESULT_OK + }; + context.RIL.sendStkTerminalResponse(response); +}); + +/** + * Verify STK terminal response : GET INPUT with empty string. + * + * @See |TERMINAL RESPONSE: GET INPUT 1.9.1A| of 27.22.4.3.1 GET INPUT (normal) + * in TS 102 384. + */ +add_test(function test_stk_terminal_response_get_input_empty_string() { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_TERMINAL_RESPONSE); + + // Token : we don't care + this.readInt32(); + + // Data Size, 30 = 2 * (TLV_COMMAND_DETAILS_SIZE(5) + + // TLV_DEVICE_ID_SIZE(4) + + // TLV_RESULT_SIZE(3) + + // TEXT LENGTH(3)) + equal(this.readInt32(), 30); + + // Command Details, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_COMMAND_DETAILS | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 3); + equal(pduHelper.readHexOctet(), 0x01); + equal(pduHelper.readHexOctet(), STK_CMD_GET_INPUT); + equal(pduHelper.readHexOctet(), 0x00); + + // Device Identifies, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + // Result + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_RESULT | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_RESULT_OK); + + // Text + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_TEXT_STRING | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_TEXT_CODING_GSM_8BIT); + + run_next_test(); + }; + + let response = { + command: { + commandNumber: 0x01, + typeOfCommand: STK_CMD_GET_INPUT, + commandQualifier: 0x00, + options: { + minLength: 0, + maxLength: 1, + defaultText: "<SEND>" + } + }, + input: "", + resultCode: STK_RESULT_OK + }; + context.RIL.sendStkTerminalResponse(response); +}); + +/** + * Verify STK terminal response : GET INPUT with 160 unpacked characters. + * + * @See |TERMINAL RESPONSE: GET INPUT 1.8.1| of 27.22.4.3.1 GET INPUT (normal) + * in TS 102 384. + */ +add_test(function test_stk_terminal_response_get_input_160_unpacked_characters() { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + let iccPduHelper = context.ICCPDUHelper; + let TEST_TEXT_STRING = "***1111111111###" + + "***2222222222###" + + "***3333333333###" + + "***4444444444###" + + "***5555555555###" + + "***6666666666###" + + "***7777777777###" + + "***8888888888###" + + "***9999999999###" + + "***0000000000###"; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_TERMINAL_RESPONSE); + + // Token : we don't care + this.readInt32(); + + // Data Size, 352 = 2 * (TLV_COMMAND_DETAILS_SIZE(5) + + // TLV_DEVICE_ID_SIZE(4) + + // TLV_RESULT_SIZE(3) + + // TEXT LENGTH(164)) + equal(this.readInt32(), 352); + + // Command Details, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_COMMAND_DETAILS | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 3); + equal(pduHelper.readHexOctet(), 0x01); + equal(pduHelper.readHexOctet(), STK_CMD_GET_INPUT); + equal(pduHelper.readHexOctet(), 0x00); + + // Device Identifies, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + // Result + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_RESULT | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_RESULT_OK); + + // Text + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_TEXT_STRING | + COMPREHENSIONTLV_FLAG_CR); + // C-TLV Length Encoding: 161 = 0x81 0xA1 + equal(pduHelper.readHexOctet(), 0x81); + equal(pduHelper.readHexOctet(), 0xA1); + equal(pduHelper.readHexOctet(), STK_TEXT_CODING_GSM_8BIT); + equal(iccPduHelper.read8BitUnpackedToString(160), TEST_TEXT_STRING); + + run_next_test(); + }; + + let response = { + command: { + commandNumber: 0x01, + typeOfCommand: STK_CMD_GET_INPUT, + commandQualifier: 0x00, + options: { + minLength: 160, + maxLength: 160, + text: TEST_TEXT_STRING + } + }, + input: TEST_TEXT_STRING, + resultCode: STK_RESULT_OK + }; + context.RIL.sendStkTerminalResponse(response); +}); + +/** + * Verify STK terminal response : GET_INKEY - NO_RESPONSE_FROM_USER with + * duration provided. + * + * @See |27.22.4.2.8 GET INKEY (Variable Time out)| in TS 102 384. + */ +add_test(function test_stk_terminal_response_get_inkey_no_response_from_user() { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_TERMINAL_RESPONSE); + + // Token : we don't care + this.readInt32(); + + // Data Size, 32 = 2 * (TLV_COMMAND_DETAILS_SIZE(5) + + // TLV_DEVICE_ID_SIZE(4) + + // TLV_RESULT_SIZE(3) + + // DURATION(4)) + equal(this.readInt32(), 32); + + // Command Details, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_COMMAND_DETAILS | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 3); + equal(pduHelper.readHexOctet(), 0x01); + equal(pduHelper.readHexOctet(), STK_CMD_GET_INKEY); + equal(pduHelper.readHexOctet(), 0x00); + + // Device Identifies, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + // Result + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_RESULT | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_RESULT_NO_RESPONSE_FROM_USER); + + // Duration, Type-Length-Value(Time unit, Time interval) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DURATION); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_TIME_UNIT_SECOND); + equal(pduHelper.readHexOctet(), 10); + + run_next_test(); + }; + + let response = { + command: { + commandNumber: 0x01, + typeOfCommand: STK_CMD_GET_INKEY, + commandQualifier: 0x00, + options: { + duration: { + timeUnit: STK_TIME_UNIT_SECOND, + timeInterval: 10 + }, + text: 'Enter "+"' + } + }, + resultCode: STK_RESULT_NO_RESPONSE_FROM_USER + }; + context.RIL.sendStkTerminalResponse(response); +}); + +/** + * Verify STK terminal response : GET_INKEY - YES/NO request + */ +add_test(function test_stk_terminal_response_get_inkey() { + function do_test(isYesNo) { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_TERMINAL_RESPONSE); + + // Token : we don't care + this.readInt32(); + + // Data Size, 32 = 2 * (TLV_COMMAND_DETAILS_SIZE(5) + + // TLV_DEVICE_ID_SIZE(4) + + // TLV_RESULT_SIZE(3) + + // TEXT LENGTH(4)) + equal(this.readInt32(), 32); + + // Command Details, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_COMMAND_DETAILS | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 3); + equal(pduHelper.readHexOctet(), 0x01); + equal(pduHelper.readHexOctet(), STK_CMD_GET_INKEY); + equal(pduHelper.readHexOctet(), 0x04); + + // Device Identifies, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + // Result + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_RESULT | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_RESULT_OK); + + // Yes/No response + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_TEXT_STRING | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_TEXT_CODING_GSM_8BIT); + equal(pduHelper.readHexOctet(), isYesNo ? 0x01 : 0x00); + }; + + let response = { + command: { + commandNumber: 0x01, + typeOfCommand: STK_CMD_GET_INKEY, + commandQualifier: 0x04, + options: { + isYesNoRequested: true + } + }, + isYesNo: isYesNo, + resultCode: STK_RESULT_OK + }; + + context.RIL.sendStkTerminalResponse(response); + }; + + // Test "Yes" response + do_test(true); + // Test "No" response + do_test(false); + + run_next_test(); +}); + +/** + * Verify STK terminal response with additional information. + */ +add_test(function test_stk_terminal_response_with_additional_info() { + function do_test(aInfo) { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_TERMINAL_RESPONSE); + + // Token : we don't care + this.readInt32(); + + // Data Length 26 = 2 * (TLV_COMMAND_DETAILS_SIZE(5) + + // TLV_DEVICE_ID_SIZE(4) + + // TLV_RESULT_SIZE(4)) + equal(this.readInt32(), 26); + + // Command Details, Type-Length-Value(commandNumber, typeOfCommand, commandQualifier) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_COMMAND_DETAILS | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 3); + equal(pduHelper.readHexOctet(), 0x01); + equal(pduHelper.readHexOctet(), STK_CMD_DISPLAY_TEXT); + equal(pduHelper.readHexOctet(), 0x01); + + // Device Identifies, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + // Result, Type-Length-Value(General result, Additional information on result) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_RESULT | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_RESULT_TERMINAL_CRNTLY_UNABLE_TO_PROCESS); + equal(pduHelper.readHexOctet(), aInfo); + }; + + let response = { + command: { + commandNumber: 0x01, + typeOfCommand: STK_CMD_DISPLAY_TEXT, + commandQualifier: 0x01, + options: { + isHighPriority: true + } + }, + resultCode: STK_RESULT_TERMINAL_CRNTLY_UNABLE_TO_PROCESS, + additionalInformation: aInfo + }; + + context.RIL.sendStkTerminalResponse(response); + }; + + do_test(0x01); // 'Screen is busy' + + run_next_test(); +}); + +// Test ComprehensionTlvHelper + +/** + * Verify ComprehensionTlvHelper.writeLocationInfoTlv + */ +add_test(function test_write_location_info_tlv() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let tlvHelper = context.ComprehensionTlvHelper; + + // Test with 2-digit mnc, and gsmCellId obtained from UMTS network. + let loc = { + mcc: "466", + mnc: "92", + gsmLocationAreaCode : 10291, + gsmCellId: 19072823 + }; + tlvHelper.writeLocationInfoTlv(loc); + + let tag = pduHelper.readHexOctet(); + equal(tag, COMPREHENSIONTLV_TAG_LOCATION_INFO | + COMPREHENSIONTLV_FLAG_CR); + + let length = pduHelper.readHexOctet(); + equal(length, 9); + + let mcc_mnc = pduHelper.readSwappedNibbleBcdString(3); + equal(mcc_mnc, "46692"); + + let lac = (pduHelper.readHexOctet() << 8) | pduHelper.readHexOctet(); + equal(lac, 10291); + + let cellId = (pduHelper.readHexOctet() << 24) | + (pduHelper.readHexOctet() << 16) | + (pduHelper.readHexOctet() << 8) | + (pduHelper.readHexOctet()); + equal(cellId, 19072823); + + // Test with 1-digit mnc, and gsmCellId obtained from GSM network. + loc = { + mcc: "466", + mnc: "02", + gsmLocationAreaCode : 10291, + gsmCellId: 65534 + }; + tlvHelper.writeLocationInfoTlv(loc); + + tag = pduHelper.readHexOctet(); + equal(tag, COMPREHENSIONTLV_TAG_LOCATION_INFO | + COMPREHENSIONTLV_FLAG_CR); + + length = pduHelper.readHexOctet(); + equal(length, 7); + + mcc_mnc = pduHelper.readSwappedNibbleBcdString(3); + equal(mcc_mnc, "46602"); + + lac = (pduHelper.readHexOctet() << 8) | pduHelper.readHexOctet(); + equal(lac, 10291); + + cellId = (pduHelper.readHexOctet() << 8) | (pduHelper.readHexOctet()); + equal(cellId, 65534); + + // Test with 3-digit mnc, and gsmCellId obtained from GSM network. + loc = { + mcc: "466", + mnc: "222", + gsmLocationAreaCode : 10291, + gsmCellId: 65534 + }; + tlvHelper.writeLocationInfoTlv(loc); + + tag = pduHelper.readHexOctet(); + equal(tag, COMPREHENSIONTLV_TAG_LOCATION_INFO | + COMPREHENSIONTLV_FLAG_CR); + + length = pduHelper.readHexOctet(); + equal(length, 7); + + mcc_mnc = pduHelper.readSwappedNibbleBcdString(3); + equal(mcc_mnc, "466222"); + + lac = (pduHelper.readHexOctet() << 8) | pduHelper.readHexOctet(); + equal(lac, 10291); + + cellId = (pduHelper.readHexOctet() << 8) | (pduHelper.readHexOctet()); + equal(cellId, 65534); + + run_next_test(); +}); + +/** + * Verify ComprehensionTlvHelper.writeErrorNumber + */ +add_test(function test_write_disconnecting_cause() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let tlvHelper = context.ComprehensionTlvHelper; + + tlvHelper.writeCauseTlv(RIL_CALL_FAILCAUSE_TO_GECKO_CALL_ERROR[CALL_FAIL_BUSY]); + let tag = pduHelper.readHexOctet(); + equal(tag, COMPREHENSIONTLV_TAG_CAUSE | COMPREHENSIONTLV_FLAG_CR); + let len = pduHelper.readHexOctet(); + equal(len, 2); // We have one cause. + let standard = pduHelper.readHexOctet(); + equal(standard, 0x60); + let cause = pduHelper.readHexOctet(); + equal(cause, 0x80 | CALL_FAIL_BUSY); + + run_next_test(); +}); + +/** + * Verify ComprehensionTlvHelper.getSizeOfLengthOctets + */ +add_test(function test_get_size_of_length_octets() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let tlvHelper = context.ComprehensionTlvHelper; + + let length = 0x70; + equal(tlvHelper.getSizeOfLengthOctets(length), 1); + + length = 0x80; + equal(tlvHelper.getSizeOfLengthOctets(length), 2); + + length = 0x180; + equal(tlvHelper.getSizeOfLengthOctets(length), 3); + + length = 0x18000; + equal(tlvHelper.getSizeOfLengthOctets(length), 4); + + run_next_test(); +}); + +/** + * Verify ComprehensionTlvHelper.writeLength + */ +add_test(function test_write_length() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let tlvHelper = context.ComprehensionTlvHelper; + + let length = 0x70; + tlvHelper.writeLength(length); + equal(pduHelper.readHexOctet(), length); + + length = 0x80; + tlvHelper.writeLength(length); + equal(pduHelper.readHexOctet(), 0x81); + equal(pduHelper.readHexOctet(), length); + + length = 0x180; + tlvHelper.writeLength(length); + equal(pduHelper.readHexOctet(), 0x82); + equal(pduHelper.readHexOctet(), (length >> 8) & 0xff); + equal(pduHelper.readHexOctet(), length & 0xff); + + length = 0x18000; + tlvHelper.writeLength(length); + equal(pduHelper.readHexOctet(), 0x83); + equal(pduHelper.readHexOctet(), (length >> 16) & 0xff); + equal(pduHelper.readHexOctet(), (length >> 8) & 0xff); + equal(pduHelper.readHexOctet(), length & 0xff); + + run_next_test(); +}); + +// Test Proactive commands. + +function test_stk_proactive_command(aOptions) { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let berHelper = context.BerTlvHelper; + let stkHelper = context.StkProactiveCmdHelper; + let stkFactory = context.StkCommandParamsFactory; + + let testPdu = aOptions.pdu; + let testTypeOfCommand = aOptions.typeOfCommand; + let testIcons = aOptions.icons; + let testFunc = aOptions.testFunc; + + if (testIcons) { + let ril = context.RIL; + ril.iccInfoPrivate.sst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x30]; //IMG: 39 + ril.appType = CARD_APPTYPE_SIM; + + // skip asynchornous process in IconLoader.loadIcons(). + let iconLoader = context.IconLoader; + iconLoader.loadIcons = (recordNumbers, onsuccess, onerror) => { + onsuccess(testIcons); + }; + } + + for(let i = 0 ; i < testPdu.length; i++) { + pduHelper.writeHexOctet(testPdu[i]); + } + + let berTlv = berHelper.decode(testPdu.length); + let ctlvs = berTlv.value; + let ctlv = stkHelper.searchForTag(COMPREHENSIONTLV_TAG_COMMAND_DETAILS, ctlvs); + let cmdDetails = ctlv.value; + equal(cmdDetails.typeOfCommand, testTypeOfCommand); + + stkFactory.createParam(cmdDetails, ctlvs, (aResult) => { + cmdDetails.options = aResult; + testFunc(context, cmdDetails, ctlvs); + }); +} + +/** + * Verify Proactive command helper : searchForSelectedTags + */ +add_test(function test_stk_proactive_command_search_for_selected_tags() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let berHelper = context.BerTlvHelper; + let stkHelper = context.StkProactiveCmdHelper; + + let tag_test = [ + 0xD0, + 0x3E, + 0x85, 0x0A, 0x61, 0x6C, 0x70, 0x68, 0x61, 0x20, 0x69, 0x64, 0x20, 0x31, + 0x85, 0x0A, 0x61, 0x6C, 0x70, 0x68, 0x61, 0x20, 0x69, 0x64, 0x20, 0x32, + 0x85, 0x0A, 0x61, 0x6C, 0x70, 0x68, 0x61, 0x20, 0x69, 0x64, 0x20, 0x33, + 0x85, 0x0A, 0x61, 0x6C, 0x70, 0x68, 0x61, 0x20, 0x69, 0x64, 0x20, 0x34, + 0x85, 0x0A, 0x61, 0x6C, 0x70, 0x68, 0x61, 0x20, 0x69, 0x64, 0x20, 0x35, + 0x85, 0x00]; + + for (let i = 0; i < tag_test.length; i++) { + pduHelper.writeHexOctet(tag_test[i]); + } + + let berTlv = berHelper.decode(tag_test.length); + let selectedCtlvs = + stkHelper.searchForSelectedTags(berTlv.value, [COMPREHENSIONTLV_TAG_ALPHA_ID]); + let tlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + equal(tlv.value.identifier, "alpha id 1"); + + tlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + equal(tlv.value.identifier, "alpha id 2"); + + tlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + equal(tlv.value.identifier, "alpha id 3"); + + tlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + equal(tlv.value.identifier, "alpha id 4"); + + tlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + equal(tlv.value.identifier, "alpha id 5"); + + // emulate that the alpha identifier is provided and is a null data object, + // which is converted to an empty string in ICCPDUHelper. + tlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + strictEqual(tlv.value.identifier, ""); + + // emulate that the alpha identifier is not provided + tlv = selectedCtlvs.retrieve(COMPREHENSIONTLV_TAG_ALPHA_ID); + strictEqual(tlv, undefined); + + run_next_test(); +}); + +/** + * Verify Proactive Command : Refresh + */ +add_test(function test_stk_proactive_command_refresh() { + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x10, + 0x81, 0x03, 0x01, 0x01, 0x01, + 0x82, 0x02, 0x81, 0x82, + 0x92, 0x05, 0x01, 0x3F, 0x00, 0x2F, 0xE2 + ], + typeOfCommand: STK_CMD_REFRESH, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let stkHelper = context.StkProactiveCmdHelper; + let ctlv = stkHelper.searchForTag(COMPREHENSIONTLV_TAG_FILE_LIST, ctlvs); + equal(ctlv.value.fileList, "3F002FE2"); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command : Play Tone + */ +add_test(function test_stk_proactive_command_play_tone() { + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x1F, + 0x81, 0x03, 0x01, 0x20, 0x00, + 0x82, 0x02, 0x81, 0x03, + 0x85, 0x09, 0x44, 0x69, 0x61, 0x6C, 0x20, 0x54, 0x6F, 0x6E, 0x65, + 0x8E, 0x01, 0x01, + 0x84, 0x02, 0x01, 0x05, + 0x9E, 0x02, 0x00, 0x01 + ], + typeOfCommand: STK_CMD_PLAY_TONE, + icons: [1], + testFunc: (context, cmdDetails, ctlvs) => { + let playTone = cmdDetails.options; + + equal(playTone.text, "Dial Tone"); + equal(playTone.tone, STK_TONE_TYPE_DIAL_TONE); + equal(playTone.duration.timeUnit, STK_TIME_UNIT_SECOND); + equal(playTone.duration.timeInterval, 5); + equal(playTone.iconSelfExplanatory, true); + equal(playTone.icons, 1); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command : Poll Interval + */ +add_test(function test_stk_proactive_command_poll_interval() { + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x0D, + 0x81, 0x03, 0x01, 0x03, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x84, 0x02, 0x01, 0x14 + ], + typeOfCommand: STK_CMD_POLL_INTERVAL, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let interval = cmdDetails.options; + + equal(interval.timeUnit, STK_TIME_UNIT_SECOND); + equal(interval.timeInterval, 0x14); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command: Display Text + */ +add_test(function test_stk_proactive_command_display_text() { + test_stk_proactive_command({ + pdu: [ + 0xd0, + 0x2c, + 0x81, 0x03, 0x01, 0x21, 0x80, + 0x82, 0x02, 0x81, 0x02, + 0x0d, 0x1d, 0x00, 0xd3, 0x30, 0x9b, 0xfc, 0x06, 0xc9, 0x5c, 0x30, 0x1a, + 0xa8, 0xe8, 0x02, 0x59, 0xc3, 0xec, 0x34, 0xb9, 0xac, 0x07, 0xc9, 0x60, + 0x2f, 0x58, 0xed, 0x15, 0x9b, 0xb9, 0x40, + 0x9e, 0x02, 0x00, 0x01 + ], + typeOfCommand: STK_CMD_DISPLAY_TEXT, + icons: [1], + testFunc: (context, cmdDetails, ctlvs) => { + let textMsg = cmdDetails.options; + + equal(textMsg.text, "Saldo 2.04 E. Validez 20/05/13. "); + equal(textMsg.iconSelfExplanatory, true); + equal(textMsg.icons, 1); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command: Set Up Event List. + */ +add_test(function test_stk_proactive_command_event_list() { + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x0F, + 0x81, 0x03, 0x01, 0x05, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x99, 0x04, 0x00, 0x01, 0x02, 0x03 + ], + typeOfCommand: STK_CMD_SET_UP_EVENT_LIST, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let event = cmdDetails.options; + + equal(Array.isArray(event.eventList), true); + + for (let i = 0; i < event.eventList.length; i++) { + equal(event.eventList[i], i); + } + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command : Get Input + */ +add_test(function test_stk_proactive_command_get_input() { + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x22, + 0x81, 0x03, 0x01, 0x23, 0x8F, + 0x82, 0x02, 0x81, 0x82, + 0x8D, 0x05, 0x04, 0x54, 0x65, 0x78, 0x74, + 0x91, 0x02, 0x01, 0x10, + 0x17, 0x08, 0x04, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6C, 0x74, + 0x9E, 0x02, 0x00, 0x01 + ], + typeOfCommand: STK_CMD_GET_INPUT, + icons: [1], + testFunc: (context, cmdDetails, ctlvs) => { + let input = cmdDetails.options; + + equal(input.text, "Text"); + equal(input.isAlphabet, true); + equal(input.isUCS2, true); + equal(input.hideInput, true); + equal(input.isPacked, true); + equal(input.isHelpAvailable, true); + equal(input.minLength, 0x01); + equal(input.maxLength, 0x10); + equal(input.defaultText, "Default"); + equal(input.iconSelfExplanatory, true); + equal(input.icons, 1); + } + }); + + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x11, + 0x81, 0x03, 0x01, 0x23, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x8D, 0x00, + 0x91, 0x02, 0x01, 0x10, + 0x17, 0x00 + ], + typeOfCommand: STK_CMD_GET_INPUT, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let input = cmdDetails.options; + + equal(input.text, null); + equal(input.minLength, 0x01); + equal(input.maxLength, 0x10); + equal(input.defaultText, null); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command : More Time + */ +add_test(function test_stk_proactive_command_more_time() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let berHelper = context.BerTlvHelper; + let stkHelper = context.StkProactiveCmdHelper; + + let more_time_1 = [ + 0xD0, + 0x09, + 0x81, 0x03, 0x01, 0x02, 0x00, + 0x82, 0x02, 0x81, 0x82]; + + for(let i = 0 ; i < more_time_1.length; i++) { + pduHelper.writeHexOctet(more_time_1[i]); + } + + let berTlv = berHelper.decode(more_time_1.length); + let ctlvs = berTlv.value; + let tlv = stkHelper.searchForTag(COMPREHENSIONTLV_TAG_COMMAND_DETAILS, ctlvs); + equal(tlv.value.commandNumber, 0x01); + equal(tlv.value.typeOfCommand, STK_CMD_MORE_TIME); + equal(tlv.value.commandQualifier, 0x00); + + run_next_test(); +}); + +/** + * Verify Proactive Command : Select Item + */ +add_test(function test_stk_proactive_command_select_item() { + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x3D, + 0x81, 0x03, 0x01, 0x24, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x85, 0x05, 0x54, 0x69, 0x74, 0x6C, 0x65, + 0x8F, 0x07, 0x01, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x31, + 0x8F, 0x07, 0x02, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x32, + 0x8F, 0x07, 0x03, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x33, + 0x18, 0x03, 0x10, 0x15, 0x20, + 0x90, 0x01, 0x01, + 0x9E, 0x02, 0x00, 0x01, + 0x9F, 0x04, 0x00, 0x01, 0x02, 0x03 + ], + typeOfCommand: STK_CMD_SELECT_ITEM, + icons: [1, 1, 2, 3], + testFunc: (context, cmdDetails, ctlvs) => { + let menu = cmdDetails.options; + + equal(menu.title, "Title"); + equal(menu.iconSelfExplanatory, true); + equal(menu.icons, 1); + equal(menu.items[0].identifier, 1); + equal(menu.items[0].text, "item 1"); + equal(menu.items[0].iconSelfExplanatory, true); + equal(menu.items[0].icons, 1); + equal(menu.items[1].identifier, 2); + equal(menu.items[1].text, "item 2"); + equal(menu.items[1].iconSelfExplanatory, true); + equal(menu.items[1].icons, 2); + equal(menu.items[2].identifier, 3); + equal(menu.items[2].text, "item 3"); + equal(menu.items[2].iconSelfExplanatory, true); + equal(menu.items[2].icons, 3); + equal(menu.nextActionList[0], STK_CMD_SET_UP_CALL); + equal(menu.nextActionList[1], STK_CMD_LAUNCH_BROWSER); + equal(menu.nextActionList[2], STK_CMD_PLAY_TONE); + equal(menu.defaultItem, 0x00); + } + }); + + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x33, + 0x81, 0x03, 0x01, 0x24, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x85, 0x05, 0x54, 0x69, 0x74, 0x6C, 0x65, + 0x8F, 0x07, 0x01, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x31, + 0x8F, 0x07, 0x02, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x32, + 0x8F, 0x07, 0x03, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x33, + 0x18, 0x03, 0x00, 0x15, 0x81, + 0x90, 0x01, 0x03 + ], + typeOfCommand: STK_CMD_SELECT_ITEM, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let menu = cmdDetails.options; + + equal(menu.title, "Title"); + equal(menu.items[0].identifier, 1); + equal(menu.items[0].text, "item 1"); + equal(menu.items[1].identifier, 2); + equal(menu.items[1].text, "item 2"); + equal(menu.items[2].identifier, 3); + equal(menu.items[2].text, "item 3"); + equal(menu.nextActionList[0], STK_NEXT_ACTION_NULL); + equal(menu.nextActionList[1], STK_CMD_LAUNCH_BROWSER); + equal(menu.nextActionList[2], STK_NEXT_ACTION_END_PROACTIVE_SESSION); + equal(menu.defaultItem, 0x02); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command : Set Up Menu + */ +add_test(function test_stk_proactive_command_set_up_menu() { + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x3A, + 0x81, 0x03, 0x01, 0x25, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x85, 0x05, 0x54, 0x69, 0x74, 0x6C, 0x65, + 0x8F, 0x07, 0x01, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x31, + 0x8F, 0x07, 0x02, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x32, + 0x8F, 0x07, 0x03, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x33, + 0x18, 0x03, 0x10, 0x15, 0x20, + 0x9E, 0x02, 0x00, 0x01, + 0x9F, 0x04, 0x00, 0x01, 0x02, 0x03 + ], + typeOfCommand: STK_CMD_SET_UP_MENU, + icons: [1, 1, 2, 3], + testFunc: (context, cmdDetails, ctlvs) => { + let menu = cmdDetails.options; + + equal(menu.title, "Title"); + equal(menu.iconSelfExplanatory, true); + equal(menu.icons, 1); + equal(menu.items[0].identifier, 1); + equal(menu.items[0].text, "item 1"); + equal(menu.items[0].iconSelfExplanatory, true); + equal(menu.items[0].icons, 1); + equal(menu.items[1].identifier, 2); + equal(menu.items[1].text, "item 2"); + equal(menu.items[1].iconSelfExplanatory, true); + equal(menu.items[1].icons, 2); + equal(menu.items[2].identifier, 3); + equal(menu.items[2].text, "item 3"); + equal(menu.items[2].iconSelfExplanatory, true); + equal(menu.items[2].icons, 3); + equal(menu.nextActionList[0], STK_CMD_SET_UP_CALL); + equal(menu.nextActionList[1], STK_CMD_LAUNCH_BROWSER); + equal(menu.nextActionList[2], STK_CMD_PLAY_TONE); + } + }); + + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x30, + 0x81, 0x03, 0x01, 0x25, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x85, 0x05, 0x54, 0x69, 0x74, 0x6C, 0x65, + 0x8F, 0x07, 0x01, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x31, + 0x8F, 0x07, 0x02, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x32, + 0x8F, 0x07, 0x03, 0x69, 0x74, 0x65, 0x6D, 0x20, 0x33, + 0x18, 0x03, 0x81, 0x00, 0x00 + ], + typeOfCommand: STK_CMD_SET_UP_MENU, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let menu = cmdDetails.options; + + equal(menu.title, "Title"); + equal(menu.items[0].identifier, 1); + equal(menu.items[0].text, "item 1"); + equal(menu.items[1].identifier, 2); + equal(menu.items[1].text, "item 2"); + equal(menu.items[2].identifier, 3); + equal(menu.items[2].text, "item 3"); + equal(menu.nextActionList[0], STK_NEXT_ACTION_END_PROACTIVE_SESSION); + equal(menu.nextActionList[1], STK_NEXT_ACTION_NULL); + equal(menu.nextActionList[2], STK_NEXT_ACTION_NULL); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command : Set Up Call + */ +add_test(function test_stk_proactive_command_set_up_call() { + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x31, + 0x81, 0x03, 0x01, 0x10, 0x04, + 0x82, 0x02, 0x81, 0x82, + 0x05, 0x0A, 0x44, 0x69, 0x73, 0x63, 0x6F, 0x6E, 0x6E, 0x65, 0x63, 0x74, + 0x86, 0x09, 0x81, 0x10, 0x32, 0x04, 0x21, 0x43, 0x65, 0x1C, 0x2C, + 0x05, 0x07, 0x4D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x9E, 0x02, 0x00, 0x01, + 0x9E, 0x02, 0x01, 0x02 + ], + typeOfCommand: STK_CMD_SET_UP_CALL, + icons: [1, 2], + testFunc: (context, cmdDetails, ctlvs) => { + let setupCall = cmdDetails.options; + + equal(setupCall.address, "012340123456,1,2"); + equal(setupCall.confirmMessage.text, "Disconnect"); + equal(setupCall.confirmMessage.iconSelfExplanatory, true); + equal(setupCall.confirmMessage.icons, 1); + equal(setupCall.callMessage.text, "Message"); + equal(setupCall.callMessage.iconSelfExplanatory, false); + equal(setupCall.callMessage.icons, 2); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command : Timer Management + */ +add_test(function test_stk_proactive_command_timer_management() { + // Timer Management - Start + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x11, + 0x81, 0x03, 0x01, 0x27, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0xA4, 0x01, 0x01, + 0xA5, 0x03, 0x10, 0x20, 0x30 + ], + typeOfCommand: STK_CMD_TIMER_MANAGEMENT, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + equal(cmdDetails.commandQualifier, STK_TIMER_START); + + let timer = cmdDetails.options; + + equal(timer.timerId, 0x01); + equal(timer.timerValue, (0x01 * 60 * 60) + (0x02 * 60) + 0x03); + } + }); + + // Timer Management - Deactivate + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x0C, + 0x81, 0x03, 0x01, 0x27, 0x01, + 0x82, 0x02, 0x81, 0x82, + 0xA4, 0x01, 0x01 + ], + typeOfCommand: STK_CMD_TIMER_MANAGEMENT, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + equal(cmdDetails.commandQualifier, STK_TIMER_DEACTIVATE); + + let timer = cmdDetails.options; + + equal(timer.timerId, 0x01); + ok(timer.timerValue === undefined); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive Command : Provide Local Information + */ +add_test(function test_stk_proactive_command_provide_local_information() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let berHelper = context.BerTlvHelper; + let stkHelper = context.StkProactiveCmdHelper; + let stkCmdHelper = context.StkCommandParamsFactory; + + // Verify IMEI + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x09, + 0x81, 0x03, 0x01, 0x26, 0x01, + 0x82, 0x02, 0x81, 0x82 + ], + typeOfCommand: STK_CMD_PROVIDE_LOCAL_INFO, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + equal(cmdDetails.commandQualifier, STK_LOCAL_INFO_IMEI); + + let provideLocalInfo = cmdDetails.options; + equal(provideLocalInfo.localInfoType, STK_LOCAL_INFO_IMEI); + } + }); + + // Verify Date and Time Zone + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x09, + 0x81, 0x03, 0x01, 0x26, 0x03, + 0x82, 0x02, 0x81, 0x82 + ], + typeOfCommand: STK_CMD_PROVIDE_LOCAL_INFO, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + equal(cmdDetails.commandQualifier, STK_LOCAL_INFO_DATE_TIME_ZONE); + + let provideLocalInfo = cmdDetails.options; + equal(provideLocalInfo.localInfoType, STK_LOCAL_INFO_DATE_TIME_ZONE); + } + }); + + run_next_test(); +}); + +/** + * Verify Proactive command : BIP Messages + */ +add_test(function test_stk_proactive_command_open_channel() { + let worker = newUint8Worker(); + let context = worker.ContextPool._contexts[0]; + let pduHelper = context.GsmPDUHelper; + let berHelper = context.BerTlvHelper; + let stkHelper = context.StkProactiveCmdHelper; + let stkCmdHelper = context.StkCommandParamsFactory; + + // Open Channel + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x0F, + 0x81, 0x03, 0x01, 0x40, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x85, 0x04, 0x4F, 0x70, 0x65, 0x6E //alpha id: "Open" + ], + typeOfCommand: STK_CMD_OPEN_CHANNEL, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let bipMsg = cmdDetails.options; + + equal(bipMsg.text, "Open"); + } + }); + + // Close Channel + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x10, + 0x81, 0x03, 0x01, 0x41, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x85, 0x05, 0x43, 0x6C, 0x6F, 0x73, 0x65 //alpha id: "Close" + ], + typeOfCommand: STK_CMD_CLOSE_CHANNEL, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let bipMsg = cmdDetails.options; + + equal(bipMsg.text, "Close"); + } + }); + + // Receive Data + test_stk_proactive_command({ + pdu: [ + 0XD0, + 0X12, + 0x81, 0x03, 0x01, 0x42, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x85, 0x07, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65 //alpha id: "Receive" + ], + typeOfCommand: STK_CMD_RECEIVE_DATA, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let bipMsg = cmdDetails.options; + + equal(bipMsg.text, "Receive"); + } + }); + + // Send Data + test_stk_proactive_command({ + pdu: [ + 0xD0, + 0x0F, + 0x81, 0x03, 0x01, 0x43, 0x00, + 0x82, 0x02, 0x81, 0x82, + 0x85, 0x04, 0x53, 0x65, 0x6E, 0x64 //alpha id: "Send" + ], + typeOfCommand: STK_CMD_SEND_DATA, + icons: null, + testFunc: (context, cmdDetails, ctlvs) => { + let bipMsg = cmdDetails.options; + + equal(bipMsg.text, "Send"); + } + }); + + run_next_test(); +}); + +/** + * Verify Event Download Command : Location Status + */ +add_test(function test_stk_event_download_location_status() { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_ENVELOPE_COMMAND); + + // Token : we don't care + this.readInt32(); + + // Data Size, 42 = 2 * (2 + TLV_DEVICE_ID_SIZE(4) + + // TLV_EVENT_LIST_SIZE(3) + + // TLV_LOCATION_STATUS_SIZE(3) + + // TLV_LOCATION_INFO_GSM_SIZE(9)) + equal(this.readInt32(), 42); + + // BER tag + equal(pduHelper.readHexOctet(), BER_EVENT_DOWNLOAD_TAG); + + // BER length, 19 = TLV_DEVICE_ID_SIZE(4) + + // TLV_EVENT_LIST_SIZE(3) + + // TLV_LOCATION_STATUS_SIZE(3) + + // TLV_LOCATION_INFO_GSM_SIZE(9) + equal(pduHelper.readHexOctet(), 19); + + // Event List, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_EVENT_LIST | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_EVENT_TYPE_LOCATION_STATUS); + + // Device Identifies, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + // Location Status, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_LOCATION_STATUS | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_SERVICE_STATE_NORMAL); + + // Location Info, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_LOCATION_INFO | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 7); + + equal(pduHelper.readHexOctet(), 0x21); // MCC + MNC + equal(pduHelper.readHexOctet(), 0x63); + equal(pduHelper.readHexOctet(), 0x54); + equal(pduHelper.readHexOctet(), 0); // LAC + equal(pduHelper.readHexOctet(), 0); + equal(pduHelper.readHexOctet(), 0); // Cell ID + equal(pduHelper.readHexOctet(), 0); + + run_next_test(); + }; + + let event = { + eventType: STK_EVENT_TYPE_LOCATION_STATUS, + locationStatus: STK_SERVICE_STATE_NORMAL, + locationInfo: { + mcc: "123", + mnc: "456", + gsmLocationAreaCode: 0, + gsmCellId: 0 + } + }; + context.RIL.sendStkEventDownload({ + event: event + }); +}); + +// Test Event Download commands. + +/** + * Verify Event Download Command : Language Selection + */ +add_test(function test_stk_event_download_language_selection() { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + let iccHelper = context.ICCPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_ENVELOPE_COMMAND); + + // Token : we don't care + this.readInt32(); + + // Data Size, 26 = 2 * (2 + TLV_DEVICE_ID_SIZE(4) + + // TLV_EVENT_LIST_SIZE(3) + + // TLV_LANGUAGE(4)) + equal(this.readInt32(), 26); + + // BER tag + equal(pduHelper.readHexOctet(), BER_EVENT_DOWNLOAD_TAG); + + // BER length, 19 = TLV_DEVICE_ID_SIZE(4) + + // TLV_EVENT_LIST_SIZE(3) + + // TLV_LANGUAGE(4) + equal(pduHelper.readHexOctet(), 11); + + // Event List, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_EVENT_LIST | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_EVENT_TYPE_LANGUAGE_SELECTION); + + // Device Identifies, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + // Language, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_LANGUAGE); + equal(pduHelper.readHexOctet(), 2); + equal(iccHelper.read8BitUnpackedToString(2), "zh"); + + run_next_test(); + }; + + let event = { + eventType: STK_EVENT_TYPE_LANGUAGE_SELECTION, + language: "zh" + }; + context.RIL.sendStkEventDownload({ + event: event + }); +}); + +/** + * Verify Event Download Command : User Activity + */ +add_test(function test_stk_event_download_user_activity() { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_ENVELOPE_COMMAND); + + // Token : we don't care + this.readInt32(); + + // Data Size, 18 = 2 * (2 + TLV_DEVICE_ID_SIZE(4) + TLV_EVENT_LIST_SIZE(3)) + equal(this.readInt32(), 18); + + // BER tag + equal(pduHelper.readHexOctet(), BER_EVENT_DOWNLOAD_TAG); + + // BER length, 7 = TLV_DEVICE_ID_SIZE(4) + TLV_EVENT_LIST_SIZE(3) + equal(pduHelper.readHexOctet(), 7); + + // Event List, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_EVENT_LIST | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_EVENT_TYPE_USER_ACTIVITY); + + // Device Identities, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + run_next_test(); + }; + + let event = { + eventType: STK_EVENT_TYPE_USER_ACTIVITY + }; + context.RIL.sendStkEventDownload({ + event: event + }); +}); + +/** + * Verify Event Download Command : Idle Screen Available + */ +add_test(function test_stk_event_download_idle_screen_available() { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_ENVELOPE_COMMAND); + + // Token : we don't care + this.readInt32(); + + // Data Size, 18 = 2 * (2 + TLV_DEVICE_ID_SIZE(4) + TLV_EVENT_LIST_SIZE(3)) + equal(this.readInt32(), 18); + + // BER tag + equal(pduHelper.readHexOctet(), BER_EVENT_DOWNLOAD_TAG); + + // BER length, 7 = TLV_DEVICE_ID_SIZE(4) + TLV_EVENT_LIST_SIZE(3) + equal(pduHelper.readHexOctet(), 7); + + // Event List, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_EVENT_LIST | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_EVENT_TYPE_IDLE_SCREEN_AVAILABLE); + + // Device Identities, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_DISPLAY); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + run_next_test(); + }; + + let event = { + eventType: STK_EVENT_TYPE_IDLE_SCREEN_AVAILABLE + }; + context.RIL.sendStkEventDownload({ + event: event + }); +}); + +/** + * Verify Event Downloaded Command :Browser Termination + */ +add_test(function test_stk_event_download_browser_termination() { + let worker = newUint8SupportOutgoingIndexWorker(); + let context = worker.ContextPool._contexts[0]; + let buf = context.Buf; + let pduHelper = context.GsmPDUHelper; + + buf.sendParcel = function() { + // Type + equal(this.readInt32(), REQUEST_STK_SEND_ENVELOPE_COMMAND); + + // Token : we don't care + this.readInt32(); + + // Data Size, 24 = 2 * ( 2+TLV_DEVICE_ID(4)+TLV_EVENT_LIST_SIZE(3) + // +TLV_BROWSER_TERMINATION_CAUSE(3) ) + equal(this.readInt32(), 24); + + // BER tag + equal(pduHelper.readHexOctet(), BER_EVENT_DOWNLOAD_TAG); + + // BER length, 10 = TLV_DEVICE_ID(4)+TLV_EVENT_LIST_SIZE(3) + // ++TLV_BROWSER_TERMINATION_CAUSE(3) + equal(pduHelper.readHexOctet(), 10); + + // Event List, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_EVENT_LIST | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_EVENT_TYPE_BROWSER_TERMINATION); + + // Device Identities, Type-Length-Value(Source ID-Destination ID) + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_DEVICE_ID | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 2); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_ME); + equal(pduHelper.readHexOctet(), STK_DEVICE_ID_SIM); + + // Browser Termination Case, Type-Length-Value + equal(pduHelper.readHexOctet(), COMPREHENSIONTLV_TAG_BROWSER_TERMINATION_CAUSE | + COMPREHENSIONTLV_FLAG_CR); + equal(pduHelper.readHexOctet(), 1); + equal(pduHelper.readHexOctet(), STK_BROWSER_TERMINATION_CAUSE_USER); + + run_next_test(); + }; + + let event = { + eventType: STK_EVENT_TYPE_BROWSER_TERMINATION, + terminationCause: STK_BROWSER_TERMINATION_CAUSE_USER + }; + context.RIL.sendStkEventDownload({ + event: event + }); +}); diff --git a/dom/system/gonk/tests/test_ril_worker_voiceprivacy.js b/dom/system/gonk/tests/test_ril_worker_voiceprivacy.js new file mode 100644 index 000000000..21829da22 --- /dev/null +++ b/dom/system/gonk/tests/test_ril_worker_voiceprivacy.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +subscriptLoader.loadSubScript("resource://gre/modules/ril_consts.js", this); + +function run_test() { + run_next_test(); +} + +add_test(function test_setVoicePrivacyMode_success() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL.setVoicePrivacyMode = function fakeSetVoicePrivacyMode(options) { + context.RIL[REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE](0, {}); + }; + + context.RIL.setVoicePrivacyMode({ + enabled: true + }); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + + run_next_test(); +}); + +add_test(function test_setVoicePrivacyMode_generic_failure() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.RIL.setVoicePrivacyMode = function fakeSetVoicePrivacyMode(options) { + context.RIL[REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE](0, { + errorMsg: GECKO_ERROR_GENERIC_FAILURE + }); + }; + + context.RIL.setVoicePrivacyMode({ + enabled: true + }); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, GECKO_ERROR_GENERIC_FAILURE); + + run_next_test(); +}); + +add_test(function test_queryVoicePrivacyMode_success_enabled_true() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.Buf.readInt32List = function fakeReadUint32List() { + return [1]; + }; + + context.RIL.queryVoicePrivacyMode = function fakeQueryVoicePrivacyMode(options) { + context.RIL[REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE](1, {}); + }; + + context.RIL.queryVoicePrivacyMode(); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + ok(postedMessage.enabled); + run_next_test(); +}); + +add_test(function test_queryVoicePrivacyMode_success_enabled_false() { + let workerHelper = newInterceptWorker(); + let worker = workerHelper.worker; + let context = worker.ContextPool._contexts[0]; + + context.Buf.readInt32List = function fakeReadUint32List() { + return [0]; + }; + + context.RIL.queryVoicePrivacyMode = function fakeQueryVoicePrivacyMode(options) { + context.RIL[REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE](1, {}); + }; + + context.RIL.queryVoicePrivacyMode(); + + let postedMessage = workerHelper.postedMessage; + + equal(postedMessage.errorMsg, undefined); + ok(!postedMessage.enabled); + run_next_test(); +}); diff --git a/dom/system/gonk/tests/xpcshell.ini b/dom/system/gonk/tests/xpcshell.ini new file mode 100644 index 000000000..1e8b798a3 --- /dev/null +++ b/dom/system/gonk/tests/xpcshell.ini @@ -0,0 +1,43 @@ +[DEFAULT] +head = header_helpers.js +tail = + +[test_ril_worker_buf.js] +[test_ril_worker_icc_CardLock.js] +[test_ril_worker_icc_CardState.js] +[test_ril_worker_icc_BerTlvHelper.js] +[test_ril_worker_icc_GsmPDUHelper.js] +[test_ril_worker_icc_ICCContactHelper.js] +[test_ril_worker_icc_ICCIOHelper.js] +[test_ril_worker_icc_ICCPDUHelper.js] +[test_ril_worker_icc_ICCRecordHelper.js] +[test_ril_worker_icc_IconLoader.js] +[test_ril_worker_icc_ICCUtilsHelper.js] +[test_ril_worker_icc_SimRecordHelper.js] +[test_ril_worker_sms.js] +# Bug 916067 - B2G RIL: test_ril_worker_sms.js takes too long to finish +skip-if = true +[test_ril_worker_sms_cdma.js] +[test_ril_worker_sms_cdmapduhelper.js] +[test_ril_worker_sms_nl_tables.js] +[test_ril_worker_sms_gsmpduhelper.js] +[test_ril_worker_sms_segment_info.js] +[test_ril_worker_smsc_address.js] +[test_ril_worker_cf.js] +[test_ril_worker_cellbroadcast_config.js] +[test_ril_worker_cellbroadcast_gsm.js] +[test_ril_worker_cellbroadcast_umts.js] +[test_ril_worker_ruim.js] +[test_ril_worker_cw.js] +[test_ril_worker_clir.js] +[test_ril_worker_clip.js] +[test_ril_worker_ssn.js] +[test_ril_worker_voiceprivacy.js] +[test_ril_worker_ecm.js] +[test_ril_worker_stk.js] +requesttimeoutfactor = 4 +[test_ril_worker_barring_password.js] +[test_ril_worker_cdma_info_rec.js] +[test_ril_system_messenger.js] +# header_helpers.js is not needed for test_ril_system_messenger.js +head = diff --git a/dom/system/gonk/worker_buf.js b/dom/system/gonk/worker_buf.js new file mode 100644 index 000000000..7064eeac5 --- /dev/null +++ b/dom/system/gonk/worker_buf.js @@ -0,0 +1,623 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +/** + * This object contains helpers buffering incoming data & deconstructing it + * into parcels as well as buffering outgoing data & constructing parcels. + * For that it maintains two buffers and corresponding uint8 views, indexes. + * + * The incoming buffer is a circular buffer where we store incoming data. + * As soon as a complete parcel is received, it is processed right away, so + * the buffer only needs to be large enough to hold one parcel. + * + * The outgoing buffer is to prepare outgoing parcels. The index is reset + * every time a parcel is sent. + */ + +var Buf = { + INT32_MAX: 2147483647, + UINT8_SIZE: 1, + UINT16_SIZE: 2, + UINT32_SIZE: 4, + PARCEL_SIZE_SIZE: 4, + PDU_HEX_OCTET_SIZE: 4, + + incomingBufferLength: 1024, + incomingBuffer: null, + incomingBytes: null, + incomingWriteIndex: 0, + incomingReadIndex: 0, + readIncoming: 0, + readAvailable: 0, + currentParcelSize: 0, + + outgoingBufferLength: 1024, + outgoingBuffer: null, + outgoingBytes: null, + outgoingIndex: 0, + outgoingBufferCalSizeQueue: null, + + _init: function() { + this.incomingBuffer = new ArrayBuffer(this.incomingBufferLength); + this.outgoingBuffer = new ArrayBuffer(this.outgoingBufferLength); + + this.incomingBytes = new Uint8Array(this.incomingBuffer); + this.outgoingBytes = new Uint8Array(this.outgoingBuffer); + + // Track where incoming data is read from and written to. + this.incomingWriteIndex = 0; + this.incomingReadIndex = 0; + + // Leave room for the parcel size for outgoing parcels. + this.outgoingIndex = this.PARCEL_SIZE_SIZE; + + // How many bytes we've read for this parcel so far. + this.readIncoming = 0; + + // How many bytes available as parcel data. + this.readAvailable = 0; + + // Size of the incoming parcel. If this is zero, we're expecting a new + // parcel. + this.currentParcelSize = 0; + + // Queue for storing outgoing override points + this.outgoingBufferCalSizeQueue = []; + }, + + /** + * Mark current outgoingIndex as start point for calculation length of data + * written to outgoingBuffer. + * Mark can be nested for here uses queue to remember marks. + * + * @param writeFunction + * Function to write data length into outgoingBuffer, this function is + * also used to allocate buffer for data length. + * Raw data size(in Uint8) is provided as parameter calling writeFunction. + * If raw data size is not in proper unit for writing, user can adjust + * the length value in writeFunction before writing. + **/ + startCalOutgoingSize: function(writeFunction) { + let sizeInfo = {index: this.outgoingIndex, + write: writeFunction}; + + // Allocate buffer for data lemgtj. + writeFunction.call(0); + + // Get size of data length buffer for it is not counted into data size. + sizeInfo.size = this.outgoingIndex - sizeInfo.index; + + // Enqueue size calculation information. + this.outgoingBufferCalSizeQueue.push(sizeInfo); + }, + + /** + * Calculate data length since last mark, and write it into mark position. + **/ + stopCalOutgoingSize: function() { + let sizeInfo = this.outgoingBufferCalSizeQueue.pop(); + + // Remember current outgoingIndex. + let currentOutgoingIndex = this.outgoingIndex; + // Calculate data length, in uint8. + let writeSize = this.outgoingIndex - sizeInfo.index - sizeInfo.size; + + // Write data length to mark, use same function for allocating buffer to make + // sure there is no buffer overloading. + this.outgoingIndex = sizeInfo.index; + sizeInfo.write(writeSize); + + // Restore outgoingIndex. + this.outgoingIndex = currentOutgoingIndex; + }, + + /** + * Grow the incoming buffer. + * + * @param min_size + * Minimum new size. The actual new size will be the the smallest + * power of 2 that's larger than this number. + */ + growIncomingBuffer: function(min_size) { + if (DEBUG) { + debug("Current buffer of " + this.incomingBufferLength + + " can't handle incoming " + min_size + " bytes."); + } + let oldBytes = this.incomingBytes; + this.incomingBufferLength = + 2 << Math.floor(Math.log(min_size)/Math.log(2)); + if (DEBUG) debug("New incoming buffer size: " + this.incomingBufferLength); + this.incomingBuffer = new ArrayBuffer(this.incomingBufferLength); + this.incomingBytes = new Uint8Array(this.incomingBuffer); + if (this.incomingReadIndex <= this.incomingWriteIndex) { + // Read and write index are in natural order, so we can just copy + // the old buffer over to the bigger one without having to worry + // about the indexes. + this.incomingBytes.set(oldBytes, 0); + } else { + // The write index has wrapped around but the read index hasn't yet. + // Write whatever the read index has left to read until it would + // circle around to the beginning of the new buffer, and the rest + // behind that. + let head = oldBytes.subarray(this.incomingReadIndex); + let tail = oldBytes.subarray(0, this.incomingReadIndex); + this.incomingBytes.set(head, 0); + this.incomingBytes.set(tail, head.length); + this.incomingReadIndex = 0; + this.incomingWriteIndex += head.length; + } + if (DEBUG) { + debug("New incoming buffer size is " + this.incomingBufferLength); + } + }, + + /** + * Grow the outgoing buffer. + * + * @param min_size + * Minimum new size. The actual new size will be the the smallest + * power of 2 that's larger than this number. + */ + growOutgoingBuffer: function(min_size) { + if (DEBUG) { + debug("Current buffer of " + this.outgoingBufferLength + + " is too small."); + } + let oldBytes = this.outgoingBytes; + this.outgoingBufferLength = + 2 << Math.floor(Math.log(min_size)/Math.log(2)); + this.outgoingBuffer = new ArrayBuffer(this.outgoingBufferLength); + this.outgoingBytes = new Uint8Array(this.outgoingBuffer); + this.outgoingBytes.set(oldBytes, 0); + if (DEBUG) { + debug("New outgoing buffer size is " + this.outgoingBufferLength); + } + }, + + /** + * Functions for reading data from the incoming buffer. + * + * These are all little endian, apart from readParcelSize(); + */ + + /** + * Ensure position specified is readable. + * + * @param index + * Data position in incoming parcel, valid from 0 to + * currentParcelSize. + */ + ensureIncomingAvailable: function(index) { + if (index >= this.currentParcelSize) { + throw new Error("Trying to read data beyond the parcel end!"); + } else if (index < 0) { + throw new Error("Trying to read data before the parcel begin!"); + } + }, + + /** + * Seek in current incoming parcel. + * + * @param offset + * Seek offset in relative to current position. + */ + seekIncoming: function(offset) { + // Translate to 0..currentParcelSize + let cur = this.currentParcelSize - this.readAvailable; + + let newIndex = cur + offset; + this.ensureIncomingAvailable(newIndex); + + // ... incomingReadIndex -->| + // 0 new cur currentParcelSize + // |================|=======|====================| + // |<-- cur -->|<- readAvailable ->| + // |<-- newIndex -->|<-- new readAvailable -->| + this.readAvailable = this.currentParcelSize - newIndex; + + // Translate back: + if (this.incomingReadIndex < cur) { + // The incomingReadIndex is wrapped. + newIndex += this.incomingBufferLength; + } + newIndex += (this.incomingReadIndex - cur); + newIndex %= this.incomingBufferLength; + this.incomingReadIndex = newIndex; + }, + + readUint8Unchecked: function() { + let value = this.incomingBytes[this.incomingReadIndex]; + this.incomingReadIndex = (this.incomingReadIndex + 1) % + this.incomingBufferLength; + return value; + }, + + readUint8: function() { + // Translate to 0..currentParcelSize + let cur = this.currentParcelSize - this.readAvailable; + this.ensureIncomingAvailable(cur); + + this.readAvailable--; + return this.readUint8Unchecked(); + }, + + readUint8Array: function(length) { + // Translate to 0..currentParcelSize + let last = this.currentParcelSize - this.readAvailable; + last += (length - 1); + this.ensureIncomingAvailable(last); + + let array = new Uint8Array(length); + for (let i = 0; i < length; i++) { + array[i] = this.readUint8Unchecked(); + } + + this.readAvailable -= length; + return array; + }, + + readUint16: function() { + return this.readUint8() | this.readUint8() << 8; + }, + + readInt32: function() { + return this.readUint8() | this.readUint8() << 8 | + this.readUint8() << 16 | this.readUint8() << 24; + }, + + readInt64: function() { + // Avoid using bitwise operators as the operands of all bitwise operators + // are converted to signed 32-bit integers. + return this.readUint8() + + this.readUint8() * Math.pow(2, 8) + + this.readUint8() * Math.pow(2, 16) + + this.readUint8() * Math.pow(2, 24) + + this.readUint8() * Math.pow(2, 32) + + this.readUint8() * Math.pow(2, 40) + + this.readUint8() * Math.pow(2, 48) + + this.readUint8() * Math.pow(2, 56); + }, + + readInt32List: function() { + let length = this.readInt32(); + let ints = []; + for (let i = 0; i < length; i++) { + ints.push(this.readInt32()); + } + return ints; + }, + + readString: function() { + let string_len = this.readInt32(); + if (string_len < 0 || string_len >= this.INT32_MAX) { + return null; + } + let s = ""; + for (let i = 0; i < string_len; i++) { + s += String.fromCharCode(this.readUint16()); + } + // Strings are \0\0 delimited, but that isn't part of the length. And + // if the string length is even, the delimiter is two characters wide. + // It's insane, I know. + this.readStringDelimiter(string_len); + return s; + }, + + readStringList: function() { + let num_strings = this.readInt32(); + let strings = []; + for (let i = 0; i < num_strings; i++) { + strings.push(this.readString()); + } + return strings; + }, + + readStringDelimiter: function(length) { + let delimiter = this.readUint16(); + if (!(length & 1)) { + delimiter |= this.readUint16(); + } + if (DEBUG) { + if (delimiter !== 0) { + debug("Something's wrong, found string delimiter: " + delimiter); + } + } + }, + + readParcelSize: function() { + return this.readUint8Unchecked() << 24 | + this.readUint8Unchecked() << 16 | + this.readUint8Unchecked() << 8 | + this.readUint8Unchecked(); + }, + + /** + * Functions for writing data to the outgoing buffer. + */ + + /** + * Ensure position specified is writable. + * + * @param index + * Data position in outgoing parcel, valid from 0 to + * outgoingBufferLength. + */ + ensureOutgoingAvailable: function(index) { + if (index >= this.outgoingBufferLength) { + this.growOutgoingBuffer(index + 1); + } + }, + + writeUint8: function(value) { + this.ensureOutgoingAvailable(this.outgoingIndex); + + this.outgoingBytes[this.outgoingIndex] = value; + this.outgoingIndex++; + }, + + writeUint16: function(value) { + this.writeUint8(value & 0xff); + this.writeUint8((value >> 8) & 0xff); + }, + + writeInt32: function(value) { + this.writeUint8(value & 0xff); + this.writeUint8((value >> 8) & 0xff); + this.writeUint8((value >> 16) & 0xff); + this.writeUint8((value >> 24) & 0xff); + }, + + writeString: function(value) { + if (value == null) { + this.writeInt32(-1); + return; + } + this.writeInt32(value.length); + for (let i = 0; i < value.length; i++) { + this.writeUint16(value.charCodeAt(i)); + } + // Strings are \0\0 delimited, but that isn't part of the length. And + // if the string length is even, the delimiter is two characters wide. + // It's insane, I know. + this.writeStringDelimiter(value.length); + }, + + writeStringList: function(strings) { + this.writeInt32(strings.length); + for (let i = 0; i < strings.length; i++) { + this.writeString(strings[i]); + } + }, + + writeStringDelimiter: function(length) { + this.writeUint16(0); + if (!(length & 1)) { + this.writeUint16(0); + } + }, + + writeParcelSize: function(value) { + /** + * Parcel size will always be the first thing in the parcel byte + * array, but the last thing written. Store the current index off + * to a temporary to be reset after we write the size. + */ + let currentIndex = this.outgoingIndex; + this.outgoingIndex = 0; + this.writeUint8((value >> 24) & 0xff); + this.writeUint8((value >> 16) & 0xff); + this.writeUint8((value >> 8) & 0xff); + this.writeUint8(value & 0xff); + this.outgoingIndex = currentIndex; + }, + + copyIncomingToOutgoing: function(length) { + if (!length || (length < 0)) { + return; + } + + let translatedReadIndexEnd = + this.currentParcelSize - this.readAvailable + length - 1; + this.ensureIncomingAvailable(translatedReadIndexEnd); + + let translatedWriteIndexEnd = this.outgoingIndex + length - 1; + this.ensureOutgoingAvailable(translatedWriteIndexEnd); + + let newIncomingReadIndex = this.incomingReadIndex + length; + if (newIncomingReadIndex < this.incomingBufferLength) { + // Reading won't cause wrapping, go ahead with builtin copy. + this.outgoingBytes + .set(this.incomingBytes.subarray(this.incomingReadIndex, + newIncomingReadIndex), + this.outgoingIndex); + } else { + // Not so lucky. + newIncomingReadIndex %= this.incomingBufferLength; + this.outgoingBytes + .set(this.incomingBytes.subarray(this.incomingReadIndex, + this.incomingBufferLength), + this.outgoingIndex); + if (newIncomingReadIndex) { + let firstPartLength = this.incomingBufferLength - this.incomingReadIndex; + this.outgoingBytes.set(this.incomingBytes.subarray(0, newIncomingReadIndex), + this.outgoingIndex + firstPartLength); + } + } + + this.incomingReadIndex = newIncomingReadIndex; + this.readAvailable -= length; + this.outgoingIndex += length; + }, + + /** + * Parcel management + */ + + /** + * Write incoming data to the circular buffer. + * + * @param incoming + * Uint8Array containing the incoming data. + */ + writeToIncoming: function(incoming) { + // We don't have to worry about the head catching the tail since + // we process any backlog in parcels immediately, before writing + // new data to the buffer. So the only edge case we need to handle + // is when the incoming data is larger than the buffer size. + let minMustAvailableSize = incoming.length + this.readIncoming; + if (minMustAvailableSize > this.incomingBufferLength) { + this.growIncomingBuffer(minMustAvailableSize); + } + + // We can let the typed arrays do the copying if the incoming data won't + // wrap around the edges of the circular buffer. + let remaining = this.incomingBufferLength - this.incomingWriteIndex; + if (remaining >= incoming.length) { + this.incomingBytes.set(incoming, this.incomingWriteIndex); + } else { + // The incoming data would wrap around it. + let head = incoming.subarray(0, remaining); + let tail = incoming.subarray(remaining); + this.incomingBytes.set(head, this.incomingWriteIndex); + this.incomingBytes.set(tail, 0); + } + this.incomingWriteIndex = (this.incomingWriteIndex + incoming.length) % + this.incomingBufferLength; + }, + + /** + * Process incoming data. + * + * @param incoming + * Uint8Array containing the incoming data. + */ + processIncoming: function(incoming) { + if (DEBUG) { + debug("Received " + incoming.length + " bytes."); + debug("Already read " + this.readIncoming); + } + + this.writeToIncoming(incoming); + this.readIncoming += incoming.length; + while (true) { + if (!this.currentParcelSize) { + // We're expecting a new parcel. + if (this.readIncoming < this.PARCEL_SIZE_SIZE) { + // We don't know how big the next parcel is going to be, need more + // data. + if (DEBUG) debug("Next parcel size unknown, going to sleep."); + return; + } + this.currentParcelSize = this.readParcelSize(); + if (DEBUG) { + debug("New incoming parcel of size " + this.currentParcelSize); + } + // The size itself is not included in the size. + this.readIncoming -= this.PARCEL_SIZE_SIZE; + } + + if (this.readIncoming < this.currentParcelSize) { + // We haven't read enough yet in order to be able to process a parcel. + if (DEBUG) debug("Read " + this.readIncoming + ", but parcel size is " + + this.currentParcelSize + ". Going to sleep."); + return; + } + + // Alright, we have enough data to process at least one whole parcel. + // Let's do that. + let expectedAfterIndex = (this.incomingReadIndex + this.currentParcelSize) + % this.incomingBufferLength; + + if (DEBUG) { + let parcel; + if (expectedAfterIndex < this.incomingReadIndex) { + let head = this.incomingBytes.subarray(this.incomingReadIndex); + let tail = this.incomingBytes.subarray(0, expectedAfterIndex); + parcel = Array.slice(head).concat(Array.slice(tail)); + } else { + parcel = Array.slice(this.incomingBytes.subarray( + this.incomingReadIndex, expectedAfterIndex)); + } + debug("Parcel (size " + this.currentParcelSize + "): " + parcel); + } + + if (DEBUG) debug("We have at least one complete parcel."); + try { + this.readAvailable = this.currentParcelSize; + this.processParcel(); + } catch (ex) { + if (DEBUG) debug("Parcel handling threw " + ex + "\n" + ex.stack); + } + + // Ensure that the whole parcel was consumed. + if (this.incomingReadIndex != expectedAfterIndex) { + if (DEBUG) { + debug("Parcel handler didn't consume whole parcel, " + + Math.abs(expectedAfterIndex - this.incomingReadIndex) + + " bytes left over"); + } + this.incomingReadIndex = expectedAfterIndex; + } + this.readIncoming -= this.currentParcelSize; + this.readAvailable = 0; + this.currentParcelSize = 0; + } + }, + + /** + * Communicate with the IPC thread. + */ + sendParcel: function() { + // Compute the size of the parcel and write it to the front of the parcel + // where we left room for it. Note that he parcel size does not include + // the size itself. + let parcelSize = this.outgoingIndex - this.PARCEL_SIZE_SIZE; + this.writeParcelSize(parcelSize); + + // This assumes that postRILMessage will make a copy of the ArrayBufferView + // right away! + let parcel = this.outgoingBytes.subarray(0, this.outgoingIndex); + if (DEBUG) debug("Outgoing parcel: " + Array.slice(parcel)); + this.onSendParcel(parcel); + this.outgoingIndex = this.PARCEL_SIZE_SIZE; + }, + + getCurrentParcelSize: function() { + return this.currentParcelSize; + }, + + getReadAvailable: function() { + return this.readAvailable; + } + + /** + * Process one parcel. + * + * |processParcel| is an implementation provided incoming parcel processing + * function invoked when we have received a complete parcel. Implementation + * may call multiple read functions to extract data from the incoming buffer. + */ + //processParcel: function() { + // let something = this.readInt32(); + // ... + //}, + + /** + * Write raw data out to underlying channel. + * + * |onSendParcel| is an implementation provided stream output function + * invoked when we're really going to write something out. We assume the + * data are completely copied to some output buffer in this call and may + * be destroyed when it's done. + * + * @param parcel + * An array of numeric octet data. + */ + //onSendParcel: function(parcel) { + // ... + //} +}; + +module.exports = { Buf: Buf }; |