Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | #include "Volume.h" |
michael@0 | 6 | #include "VolumeCommand.h" |
michael@0 | 7 | #include "VolumeManager.h" |
michael@0 | 8 | #include "VolumeManagerLog.h" |
michael@0 | 9 | #include "nsIVolume.h" |
michael@0 | 10 | #include "nsXULAppAPI.h" |
michael@0 | 11 | |
michael@0 | 12 | #include <vold/ResponseCode.h> |
michael@0 | 13 | |
michael@0 | 14 | namespace mozilla { |
michael@0 | 15 | namespace system { |
michael@0 | 16 | |
michael@0 | 17 | Volume::EventObserverList Volume::mEventObserverList; |
michael@0 | 18 | |
michael@0 | 19 | // We have a feature where volumes can be locked when mounted. This |
michael@0 | 20 | // is used to prevent a volume from being shared with the PC while |
michael@0 | 21 | // it is actively being used (say for storing an update image) |
michael@0 | 22 | // |
michael@0 | 23 | // We use WakeLocks (a poor choice of name, but it does what we want) |
michael@0 | 24 | // from the PowerManagerService to determine when we're locked. |
michael@0 | 25 | // In particular we'll create a wakelock called volume-NAME-GENERATION |
michael@0 | 26 | // (where NAME is the volume name, and GENERATION is its generation |
michael@0 | 27 | // number), and if this wakelock is locked, then we'll prevent a volume |
michael@0 | 28 | // from being shared. |
michael@0 | 29 | // |
michael@0 | 30 | // Implementation Details: |
michael@0 | 31 | // |
michael@0 | 32 | // Since the AutoMounter can only control when something gets mounted |
michael@0 | 33 | // and not when it gets unmounted (for example: a user pulls the SDCard) |
michael@0 | 34 | // and because Volume and nsVolume data structures are maintained on |
michael@0 | 35 | // separate threads, we have the potential for some race conditions. |
michael@0 | 36 | // We eliminate the race conditions by introducing the concept of a |
michael@0 | 37 | // generation number. Every time a volume transitions to the Mounted |
michael@0 | 38 | // state, it gets assigned a new generation number. Whenever the state |
michael@0 | 39 | // of a Volume changes, we send the updated state and current generation |
michael@0 | 40 | // number to the main thread where it gets updated in the nsVolume. |
michael@0 | 41 | // |
michael@0 | 42 | // Since WakeLocks can only be queried from the main-thread, the |
michael@0 | 43 | // nsVolumeService looks for WakeLock status changes, and forwards |
michael@0 | 44 | // the results to the IOThread. |
michael@0 | 45 | // |
michael@0 | 46 | // If the Volume (IOThread) recieves a volume update where the generation |
michael@0 | 47 | // number mismatches, then the update is simply ignored. |
michael@0 | 48 | // |
michael@0 | 49 | // When a Volume (IOThread) initially becomes mounted, we assume it to |
michael@0 | 50 | // be locked until we get our first update from nsVolume (MainThread). |
michael@0 | 51 | static int32_t sMountGeneration = 0; |
michael@0 | 52 | |
michael@0 | 53 | // We don't get media inserted/removed events at startup. So we |
michael@0 | 54 | // assume it's present, and we'll be told that it's missing. |
michael@0 | 55 | Volume::Volume(const nsCSubstring& aName) |
michael@0 | 56 | : mMediaPresent(true), |
michael@0 | 57 | mState(nsIVolume::STATE_INIT), |
michael@0 | 58 | mName(aName), |
michael@0 | 59 | mMountGeneration(-1), |
michael@0 | 60 | mMountLocked(true), // Needs to agree with nsVolume::nsVolume |
michael@0 | 61 | mSharingEnabled(false), |
michael@0 | 62 | mCanBeShared(true), |
michael@0 | 63 | mIsSharing(false), |
michael@0 | 64 | mFormatRequested(false), |
michael@0 | 65 | mMountRequested(false), |
michael@0 | 66 | mUnmountRequested(false), |
michael@0 | 67 | mIsFormatting(false) |
michael@0 | 68 | { |
michael@0 | 69 | DBG("Volume %s: created", NameStr()); |
michael@0 | 70 | } |
michael@0 | 71 | |
michael@0 | 72 | void |
michael@0 | 73 | Volume::SetIsSharing(bool aIsSharing) |
michael@0 | 74 | { |
michael@0 | 75 | if (aIsSharing == mIsSharing) { |
michael@0 | 76 | return; |
michael@0 | 77 | } |
michael@0 | 78 | mIsSharing = aIsSharing; |
michael@0 | 79 | LOG("Volume %s: IsSharing set to %d state %s", |
michael@0 | 80 | NameStr(), (int)mIsSharing, StateStr(mState)); |
michael@0 | 81 | if (mIsSharing) { |
michael@0 | 82 | mEventObserverList.Broadcast(this); |
michael@0 | 83 | } |
michael@0 | 84 | } |
michael@0 | 85 | |
michael@0 | 86 | void |
michael@0 | 87 | Volume::SetIsFormatting(bool aIsFormatting) |
michael@0 | 88 | { |
michael@0 | 89 | if (aIsFormatting == mIsFormatting) { |
michael@0 | 90 | return; |
michael@0 | 91 | } |
michael@0 | 92 | mIsFormatting = aIsFormatting; |
michael@0 | 93 | LOG("Volume %s: IsFormatting set to %d state %s", |
michael@0 | 94 | NameStr(), (int)mIsFormatting, StateStr(mState)); |
michael@0 | 95 | if (mIsFormatting) { |
michael@0 | 96 | mEventObserverList.Broadcast(this); |
michael@0 | 97 | } |
michael@0 | 98 | } |
michael@0 | 99 | |
michael@0 | 100 | void |
michael@0 | 101 | Volume::SetMediaPresent(bool aMediaPresent) |
michael@0 | 102 | { |
michael@0 | 103 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 104 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 105 | |
michael@0 | 106 | // mMediaPresent is slightly redunant to the state, however |
michael@0 | 107 | // when media is removed (while Idle), we get the following: |
michael@0 | 108 | // 631 Volume sdcard /mnt/sdcard disk removed (179:0) |
michael@0 | 109 | // 605 Volume sdcard /mnt/sdcard state changed from 1 (Idle-Unmounted) to 0 (No-Media) |
michael@0 | 110 | // |
michael@0 | 111 | // And on media insertion, we get: |
michael@0 | 112 | // 630 Volume sdcard /mnt/sdcard disk inserted (179:0) |
michael@0 | 113 | // 605 Volume sdcard /mnt/sdcard state changed from 0 (No-Media) to 2 (Pending) |
michael@0 | 114 | // 605 Volume sdcard /mnt/sdcard state changed from 2 (Pending) to 1 (Idle-Unmounted) |
michael@0 | 115 | // |
michael@0 | 116 | // On media removal while the media is mounted: |
michael@0 | 117 | // 632 Volume sdcard /mnt/sdcard bad removal (179:1) |
michael@0 | 118 | // 605 Volume sdcard /mnt/sdcard state changed from 4 (Mounted) to 5 (Unmounting) |
michael@0 | 119 | // 605 Volume sdcard /mnt/sdcard state changed from 5 (Unmounting) to 1 (Idle-Unmounted) |
michael@0 | 120 | // 631 Volume sdcard /mnt/sdcard disk removed (179:0) |
michael@0 | 121 | // 605 Volume sdcard /mnt/sdcard state changed from 1 (Idle-Unmounted) to 0 (No-Media) |
michael@0 | 122 | // |
michael@0 | 123 | // When sharing with a PC, it goes Mounted -> Idle -> Shared |
michael@0 | 124 | // When unsharing with a PC, it goes Shared -> Idle -> Mounted |
michael@0 | 125 | // |
michael@0 | 126 | // The AutoMounter needs to know whether the media is present or not when |
michael@0 | 127 | // processing the Idle state. |
michael@0 | 128 | |
michael@0 | 129 | if (mMediaPresent == aMediaPresent) { |
michael@0 | 130 | return; |
michael@0 | 131 | } |
michael@0 | 132 | |
michael@0 | 133 | LOG("Volume: %s media %s", NameStr(), aMediaPresent ? "inserted" : "removed"); |
michael@0 | 134 | mMediaPresent = aMediaPresent; |
michael@0 | 135 | mEventObserverList.Broadcast(this); |
michael@0 | 136 | } |
michael@0 | 137 | |
michael@0 | 138 | void |
michael@0 | 139 | Volume::SetSharingEnabled(bool aSharingEnabled) |
michael@0 | 140 | { |
michael@0 | 141 | mSharingEnabled = aSharingEnabled; |
michael@0 | 142 | |
michael@0 | 143 | LOG("SetSharingMode for volume %s to %d canBeShared = %d", |
michael@0 | 144 | NameStr(), (int)mSharingEnabled, (int)mCanBeShared); |
michael@0 | 145 | } |
michael@0 | 146 | |
michael@0 | 147 | void |
michael@0 | 148 | Volume::SetFormatRequested(bool aFormatRequested) |
michael@0 | 149 | { |
michael@0 | 150 | mFormatRequested = aFormatRequested; |
michael@0 | 151 | |
michael@0 | 152 | LOG("SetFormatRequested for volume %s to %d CanBeFormatted = %d", |
michael@0 | 153 | NameStr(), (int)mFormatRequested, (int)CanBeFormatted()); |
michael@0 | 154 | } |
michael@0 | 155 | |
michael@0 | 156 | void |
michael@0 | 157 | Volume::SetMountRequested(bool aMountRequested) |
michael@0 | 158 | { |
michael@0 | 159 | mMountRequested = aMountRequested; |
michael@0 | 160 | |
michael@0 | 161 | LOG("SetMountRequested for volume %s to %d CanBeMounted = %d", |
michael@0 | 162 | NameStr(), (int)mMountRequested, (int)CanBeMounted()); |
michael@0 | 163 | } |
michael@0 | 164 | |
michael@0 | 165 | void |
michael@0 | 166 | Volume::SetUnmountRequested(bool aUnmountRequested) |
michael@0 | 167 | { |
michael@0 | 168 | mUnmountRequested = aUnmountRequested; |
michael@0 | 169 | |
michael@0 | 170 | LOG("SetUnmountRequested for volume %s to %d CanBeMounted = %d", |
michael@0 | 171 | NameStr(), (int)mUnmountRequested, (int)CanBeMounted()); |
michael@0 | 172 | } |
michael@0 | 173 | |
michael@0 | 174 | void |
michael@0 | 175 | Volume::SetState(Volume::STATE aNewState) |
michael@0 | 176 | { |
michael@0 | 177 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 178 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 179 | if (aNewState == mState) { |
michael@0 | 180 | return; |
michael@0 | 181 | } |
michael@0 | 182 | if (aNewState == nsIVolume::STATE_MOUNTED) { |
michael@0 | 183 | mMountGeneration = ++sMountGeneration; |
michael@0 | 184 | LOG("Volume %s: changing state from %s to %s @ '%s' (%d observers) " |
michael@0 | 185 | "mountGeneration = %d, locked = %d", |
michael@0 | 186 | NameStr(), StateStr(mState), |
michael@0 | 187 | StateStr(aNewState), mMountPoint.get(), mEventObserverList.Length(), |
michael@0 | 188 | mMountGeneration, (int)mMountLocked); |
michael@0 | 189 | } else { |
michael@0 | 190 | LOG("Volume %s: changing state from %s to %s (%d observers)", |
michael@0 | 191 | NameStr(), StateStr(mState), |
michael@0 | 192 | StateStr(aNewState), mEventObserverList.Length()); |
michael@0 | 193 | } |
michael@0 | 194 | |
michael@0 | 195 | switch (aNewState) { |
michael@0 | 196 | case nsIVolume::STATE_NOMEDIA: |
michael@0 | 197 | // Cover the startup case where we don't get insertion/removal events |
michael@0 | 198 | mMediaPresent = false; |
michael@0 | 199 | mIsSharing = false; |
michael@0 | 200 | mUnmountRequested = false; |
michael@0 | 201 | mMountRequested = false; |
michael@0 | 202 | break; |
michael@0 | 203 | |
michael@0 | 204 | case nsIVolume::STATE_MOUNTED: |
michael@0 | 205 | mMountRequested = false; |
michael@0 | 206 | mIsFormatting = false; |
michael@0 | 207 | mIsSharing = false; |
michael@0 | 208 | break; |
michael@0 | 209 | case nsIVolume::STATE_FORMATTING: |
michael@0 | 210 | mFormatRequested = false; |
michael@0 | 211 | mIsFormatting = true; |
michael@0 | 212 | mIsSharing = false; |
michael@0 | 213 | break; |
michael@0 | 214 | |
michael@0 | 215 | case nsIVolume::STATE_SHARED: |
michael@0 | 216 | case nsIVolume::STATE_SHAREDMNT: |
michael@0 | 217 | // Covers startup cases. Normally, mIsSharing would be set to true |
michael@0 | 218 | // when we issue the command to initiate the sharing process, but |
michael@0 | 219 | // it's conceivable that a volume could already be in a shared state |
michael@0 | 220 | // when b2g starts. |
michael@0 | 221 | mIsSharing = true; |
michael@0 | 222 | break; |
michael@0 | 223 | |
michael@0 | 224 | case nsIVolume::STATE_IDLE: |
michael@0 | 225 | break; |
michael@0 | 226 | default: |
michael@0 | 227 | break; |
michael@0 | 228 | } |
michael@0 | 229 | mState = aNewState; |
michael@0 | 230 | mEventObserverList.Broadcast(this); |
michael@0 | 231 | } |
michael@0 | 232 | |
michael@0 | 233 | void |
michael@0 | 234 | Volume::SetMountPoint(const nsCSubstring& aMountPoint) |
michael@0 | 235 | { |
michael@0 | 236 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 237 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 238 | |
michael@0 | 239 | if (mMountPoint.Equals(aMountPoint)) { |
michael@0 | 240 | return; |
michael@0 | 241 | } |
michael@0 | 242 | mMountPoint = aMountPoint; |
michael@0 | 243 | DBG("Volume %s: Setting mountpoint to '%s'", NameStr(), mMountPoint.get()); |
michael@0 | 244 | } |
michael@0 | 245 | |
michael@0 | 246 | void |
michael@0 | 247 | Volume::StartMount(VolumeResponseCallback* aCallback) |
michael@0 | 248 | { |
michael@0 | 249 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 250 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 251 | |
michael@0 | 252 | StartCommand(new VolumeActionCommand(this, "mount", "", aCallback)); |
michael@0 | 253 | } |
michael@0 | 254 | |
michael@0 | 255 | void |
michael@0 | 256 | Volume::StartUnmount(VolumeResponseCallback* aCallback) |
michael@0 | 257 | { |
michael@0 | 258 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 259 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 260 | |
michael@0 | 261 | StartCommand(new VolumeActionCommand(this, "unmount", "force", aCallback)); |
michael@0 | 262 | } |
michael@0 | 263 | |
michael@0 | 264 | void |
michael@0 | 265 | Volume::StartFormat(VolumeResponseCallback* aCallback) |
michael@0 | 266 | { |
michael@0 | 267 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 268 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 269 | |
michael@0 | 270 | StartCommand(new VolumeActionCommand(this, "format", "", aCallback)); |
michael@0 | 271 | } |
michael@0 | 272 | |
michael@0 | 273 | void |
michael@0 | 274 | Volume::StartShare(VolumeResponseCallback* aCallback) |
michael@0 | 275 | { |
michael@0 | 276 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 277 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 278 | |
michael@0 | 279 | StartCommand(new VolumeActionCommand(this, "share", "ums", aCallback)); |
michael@0 | 280 | } |
michael@0 | 281 | |
michael@0 | 282 | void |
michael@0 | 283 | Volume::StartUnshare(VolumeResponseCallback* aCallback) |
michael@0 | 284 | { |
michael@0 | 285 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 286 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 287 | |
michael@0 | 288 | StartCommand(new VolumeActionCommand(this, "unshare", "ums", aCallback)); |
michael@0 | 289 | } |
michael@0 | 290 | |
michael@0 | 291 | void |
michael@0 | 292 | Volume::StartCommand(VolumeCommand* aCommand) |
michael@0 | 293 | { |
michael@0 | 294 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 295 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 296 | |
michael@0 | 297 | VolumeManager::PostCommand(aCommand); |
michael@0 | 298 | } |
michael@0 | 299 | |
michael@0 | 300 | //static |
michael@0 | 301 | void |
michael@0 | 302 | Volume::RegisterObserver(Volume::EventObserver* aObserver) |
michael@0 | 303 | { |
michael@0 | 304 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 305 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 306 | |
michael@0 | 307 | mEventObserverList.AddObserver(aObserver); |
michael@0 | 308 | // Send an initial event to the observer (for each volume) |
michael@0 | 309 | size_t numVolumes = VolumeManager::NumVolumes(); |
michael@0 | 310 | for (size_t volIndex = 0; volIndex < numVolumes; volIndex++) { |
michael@0 | 311 | RefPtr<Volume> vol = VolumeManager::GetVolume(volIndex); |
michael@0 | 312 | aObserver->Notify(vol); |
michael@0 | 313 | } |
michael@0 | 314 | } |
michael@0 | 315 | |
michael@0 | 316 | //static |
michael@0 | 317 | void |
michael@0 | 318 | Volume::UnregisterObserver(Volume::EventObserver* aObserver) |
michael@0 | 319 | { |
michael@0 | 320 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 321 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 322 | |
michael@0 | 323 | mEventObserverList.RemoveObserver(aObserver); |
michael@0 | 324 | } |
michael@0 | 325 | |
michael@0 | 326 | //static |
michael@0 | 327 | void |
michael@0 | 328 | Volume::UpdateMountLock(const nsACString& aVolumeName, |
michael@0 | 329 | const int32_t& aMountGeneration, |
michael@0 | 330 | const bool& aMountLocked) |
michael@0 | 331 | { |
michael@0 | 332 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 333 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 334 | |
michael@0 | 335 | RefPtr<Volume> vol = VolumeManager::FindVolumeByName(aVolumeName); |
michael@0 | 336 | if (!vol || (vol->mMountGeneration != aMountGeneration)) { |
michael@0 | 337 | return; |
michael@0 | 338 | } |
michael@0 | 339 | if (vol->mMountLocked != aMountLocked) { |
michael@0 | 340 | vol->mMountLocked = aMountLocked; |
michael@0 | 341 | DBG("Volume::UpdateMountLock for '%s' to %d\n", vol->NameStr(), (int)aMountLocked); |
michael@0 | 342 | mEventObserverList.Broadcast(vol); |
michael@0 | 343 | } |
michael@0 | 344 | } |
michael@0 | 345 | |
michael@0 | 346 | void |
michael@0 | 347 | Volume::HandleVoldResponse(int aResponseCode, nsCWhitespaceTokenizer& aTokenizer) |
michael@0 | 348 | { |
michael@0 | 349 | MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); |
michael@0 | 350 | MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop()); |
michael@0 | 351 | |
michael@0 | 352 | // The volume name will have already been parsed, and the tokenizer will point |
michael@0 | 353 | // to the token after the volume name |
michael@0 | 354 | switch (aResponseCode) { |
michael@0 | 355 | case ResponseCode::VolumeListResult: { |
michael@0 | 356 | // Each line will look something like: |
michael@0 | 357 | // |
michael@0 | 358 | // sdcard /mnt/sdcard 1 |
michael@0 | 359 | // |
michael@0 | 360 | nsDependentCSubstring mntPoint(aTokenizer.nextToken()); |
michael@0 | 361 | SetMountPoint(mntPoint); |
michael@0 | 362 | nsresult errCode; |
michael@0 | 363 | nsCString state(aTokenizer.nextToken()); |
michael@0 | 364 | if (state.EqualsLiteral("X")) { |
michael@0 | 365 | // Special state for creating fake volumes which can't be shared. |
michael@0 | 366 | mCanBeShared = false; |
michael@0 | 367 | SetState(nsIVolume::STATE_MOUNTED); |
michael@0 | 368 | } else { |
michael@0 | 369 | SetState((STATE)state.ToInteger(&errCode)); |
michael@0 | 370 | } |
michael@0 | 371 | break; |
michael@0 | 372 | } |
michael@0 | 373 | |
michael@0 | 374 | case ResponseCode::VolumeStateChange: { |
michael@0 | 375 | // Format of the line looks something like: |
michael@0 | 376 | // |
michael@0 | 377 | // Volume sdcard /mnt/sdcard state changed from 7 (Shared-Unmounted) to 1 (Idle-Unmounted) |
michael@0 | 378 | // |
michael@0 | 379 | // So we parse out the state after the string " to " |
michael@0 | 380 | while (aTokenizer.hasMoreTokens()) { |
michael@0 | 381 | nsAutoCString token(aTokenizer.nextToken()); |
michael@0 | 382 | if (token.Equals("to")) { |
michael@0 | 383 | nsresult errCode; |
michael@0 | 384 | token = aTokenizer.nextToken(); |
michael@0 | 385 | SetState((STATE)token.ToInteger(&errCode)); |
michael@0 | 386 | break; |
michael@0 | 387 | } |
michael@0 | 388 | } |
michael@0 | 389 | break; |
michael@0 | 390 | } |
michael@0 | 391 | |
michael@0 | 392 | case ResponseCode::VolumeDiskInserted: |
michael@0 | 393 | SetMediaPresent(true); |
michael@0 | 394 | break; |
michael@0 | 395 | |
michael@0 | 396 | case ResponseCode::VolumeDiskRemoved: // fall-thru |
michael@0 | 397 | case ResponseCode::VolumeBadRemoval: |
michael@0 | 398 | SetMediaPresent(false); |
michael@0 | 399 | break; |
michael@0 | 400 | |
michael@0 | 401 | default: |
michael@0 | 402 | LOG("Volume: %s unrecognized reponse code (ignored)", NameStr()); |
michael@0 | 403 | break; |
michael@0 | 404 | } |
michael@0 | 405 | } |
michael@0 | 406 | |
michael@0 | 407 | } // namespace system |
michael@0 | 408 | } // namespace mozilla |