19c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey/* 29c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * Copyright (C) 2015 The Android Open Source Project 39c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * 49c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * Licensed under the Apache License, Version 2.0 (the "License"); 59c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * you may not use this file except in compliance with the License. 69c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * You may obtain a copy of the License at 79c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * 89c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * http://www.apache.org/licenses/LICENSE-2.0 99c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * 109c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * Unless required by applicable law or agreed to in writing, software 119c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * distributed under the License is distributed on an "AS IS" BASIS, 129c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 139c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * See the License for the specific language governing permissions and 149c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey * limitations under the License. 159c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey */ 169c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 17d0640f6358041f7e2657167560b357078db73526Jeff Sharkey#include "fs/Ext4.h" 18d0640f6358041f7e2657167560b357078db73526Jeff Sharkey#include "fs/F2fs.h" 199c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include "PrivateVolume.h" 203161fb3702830b586b2e36fa9ca4519f59f951b4Jeff Sharkey#include "EmulatedVolume.h" 219c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include "Utils.h" 229c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include "VolumeManager.h" 239c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include "ResponseCode.h" 249c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include "cryptfs.h" 259c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 269c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <base/stringprintf.h> 279c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <base/logging.h> 289c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <cutils/fs.h> 299c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <private/android_filesystem_config.h> 309c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 319c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <fcntl.h> 329c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <stdlib.h> 339c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <sys/mount.h> 349c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <sys/stat.h> 359c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <sys/types.h> 369c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <sys/wait.h> 379c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey#include <sys/param.h> 389c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 399c48498f4529f623650c56d03e63324c8d813032Jeff Sharkeyusing android::base::StringPrintf; 409c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 419c48498f4529f623650c56d03e63324c8d813032Jeff Sharkeynamespace android { 429c48498f4529f623650c56d03e63324c8d813032Jeff Sharkeynamespace vold { 439c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 44d0640f6358041f7e2657167560b357078db73526Jeff Sharkeystatic const unsigned int kMajorBlockMmc = 179; 45d0640f6358041f7e2657167560b357078db73526Jeff Sharkey 469c48498f4529f623650c56d03e63324c8d813032Jeff SharkeyPrivateVolume::PrivateVolume(dev_t device, const std::string& keyRaw) : 479c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey VolumeBase(Type::kPrivate), mRawDevice(device), mKeyRaw(keyRaw) { 489c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey setId(StringPrintf("private:%u,%u", major(device), minor(device))); 499c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey mRawDevPath = StringPrintf("/dev/block/vold/%s", getId().c_str()); 509c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} 519c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 529c48498f4529f623650c56d03e63324c8d813032Jeff SharkeyPrivateVolume::~PrivateVolume() { 539c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} 549c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 559c48498f4529f623650c56d03e63324c8d813032Jeff Sharkeystatus_t PrivateVolume::readMetadata() { 569c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey status_t res = ReadMetadata(mDmDevPath, mFsType, mFsUuid, mFsLabel); 57ce6a913aeac7db94a41362c63bab74092767bb3eJeff Sharkey notifyEvent(ResponseCode::VolumeFsTypeChanged, mFsType); 58ce6a913aeac7db94a41362c63bab74092767bb3eJeff Sharkey notifyEvent(ResponseCode::VolumeFsUuidChanged, mFsUuid); 59ce6a913aeac7db94a41362c63bab74092767bb3eJeff Sharkey notifyEvent(ResponseCode::VolumeFsLabelChanged, mFsLabel); 609c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return res; 619c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} 629c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 639c48498f4529f623650c56d03e63324c8d813032Jeff Sharkeystatus_t PrivateVolume::doCreate() { 649c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey if (CreateDeviceNode(mRawDevPath, mRawDevice)) { 659c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return -EIO; 669c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey } 679c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 689c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey // Recover from stale vold by tearing down any old mappings 699c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey cryptfs_revert_ext_volume(getId().c_str()); 709c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 719c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey // TODO: figure out better SELinux labels for private volumes 729c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 739c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey unsigned char* key = (unsigned char*) mKeyRaw.data(); 749c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey char crypto_blkdev[MAXPATHLEN]; 759c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey int res = cryptfs_setup_ext_volume(getId().c_str(), mRawDevPath.c_str(), 769c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey key, mKeyRaw.size(), crypto_blkdev); 779c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey mDmDevPath = crypto_blkdev; 789c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey if (res != 0) { 799c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey PLOG(ERROR) << getId() << " failed to setup cryptfs"; 809c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return -EIO; 819c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey } 829c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 839c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return OK; 849c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} 859c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 869c48498f4529f623650c56d03e63324c8d813032Jeff Sharkeystatus_t PrivateVolume::doDestroy() { 879c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey if (cryptfs_revert_ext_volume(getId().c_str())) { 889c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey LOG(ERROR) << getId() << " failed to revert cryptfs"; 899c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey } 909c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return DestroyDeviceNode(mRawDevPath); 919c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} 929c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 939c48498f4529f623650c56d03e63324c8d813032Jeff Sharkeystatus_t PrivateVolume::doMount() { 949c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey if (readMetadata()) { 959c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey LOG(ERROR) << getId() << " failed to read metadata"; 969c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return -EIO; 979c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey } 989c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 99f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey mPath = StringPrintf("/mnt/expand/%s", mFsUuid.c_str()); 1009c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey setPath(mPath); 1019c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 102f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey if (PrepareDir(mPath, 0700, AID_ROOT, AID_ROOT)) { 1039c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey PLOG(ERROR) << getId() << " failed to create mount point " << mPath; 104f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey return -EIO; 105f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey } 106f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey 107d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if (mFsType == "ext4") { 108d0640f6358041f7e2657167560b357078db73526Jeff Sharkey int res = ext4::Check(mDmDevPath, mPath); 109d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if (res == 0 || res == 1) { 110d0640f6358041f7e2657167560b357078db73526Jeff Sharkey LOG(DEBUG) << getId() << " passed filesystem check"; 111d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } else { 112d0640f6358041f7e2657167560b357078db73526Jeff Sharkey PLOG(ERROR) << getId() << " failed filesystem check"; 113d0640f6358041f7e2657167560b357078db73526Jeff Sharkey return -EIO; 114d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } 115d0640f6358041f7e2657167560b357078db73526Jeff Sharkey 116d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if (ext4::Mount(mDmDevPath, mPath, false, false, true)) { 117d0640f6358041f7e2657167560b357078db73526Jeff Sharkey PLOG(ERROR) << getId() << " failed to mount"; 118d0640f6358041f7e2657167560b357078db73526Jeff Sharkey return -EIO; 119d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } 120d0640f6358041f7e2657167560b357078db73526Jeff Sharkey 121d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } else if (mFsType == "f2fs") { 122d0640f6358041f7e2657167560b357078db73526Jeff Sharkey int res = f2fs::Check(mDmDevPath); 123d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if (res == 0) { 124d0640f6358041f7e2657167560b357078db73526Jeff Sharkey LOG(DEBUG) << getId() << " passed filesystem check"; 125d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } else { 126d0640f6358041f7e2657167560b357078db73526Jeff Sharkey PLOG(ERROR) << getId() << " failed filesystem check"; 127d0640f6358041f7e2657167560b357078db73526Jeff Sharkey return -EIO; 128d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } 129d0640f6358041f7e2657167560b357078db73526Jeff Sharkey 130d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if (f2fs::Mount(mDmDevPath, mPath)) { 131d0640f6358041f7e2657167560b357078db73526Jeff Sharkey PLOG(ERROR) << getId() << " failed to mount"; 132d0640f6358041f7e2657167560b357078db73526Jeff Sharkey return -EIO; 133d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } 1349c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 135d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } else { 136d0640f6358041f7e2657167560b357078db73526Jeff Sharkey LOG(ERROR) << getId() << " unsupported filesystem " << mFsType; 1379c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return -EIO; 1389c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey } 1399c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 14034824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey LOG(VERBOSE) << "Starting restorecon of " << mPath; 14134824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey 14234824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey // TODO: find a cleaner way of waiting for restorecon to finish 14334824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey property_set("selinux.restorecon_recursive", ""); 14434824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey property_set("selinux.restorecon_recursive", mPath.c_str()); 14534824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey 14634824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey char value[PROPERTY_VALUE_MAX]; 14734824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey while (true) { 14834824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey property_get("selinux.restorecon_recursive", value, ""); 14934824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey if (strcmp(mPath.c_str(), value) == 0) { 15034824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey break; 15134824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey } 15234824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey sleep(1); 15334824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey LOG(VERBOSE) << "Waiting for restorecon..."; 15434824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey } 15534824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey 15634824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey LOG(VERBOSE) << "Finished restorecon of " << mPath; 15734824129de2c4a8bb0d1cb9011beff2c186a87d0Jeff Sharkey 158f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey // Verify that common directories are ready to roll 159f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey if (PrepareDir(mPath + "/app", 0771, AID_SYSTEM, AID_SYSTEM) || 160f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey PrepareDir(mPath + "/user", 0711, AID_SYSTEM, AID_SYSTEM) || 161f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey PrepareDir(mPath + "/media", 0770, AID_MEDIA_RW, AID_MEDIA_RW) || 16281f55c6dc1a14ed68e404fa3a2c244dd343e4990Jeff Sharkey PrepareDir(mPath + "/media/0", 0770, AID_MEDIA_RW, AID_MEDIA_RW) || 163f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey PrepareDir(mPath + "/local", 0751, AID_ROOT, AID_ROOT) || 164f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey PrepareDir(mPath + "/local/tmp", 0771, AID_SHELL, AID_SHELL)) { 165f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey PLOG(ERROR) << getId() << " failed to prepare"; 166f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey return -EIO; 167f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey } 168f0121c574ede0898e36eeba0a0e0affd0f8d81a7Jeff Sharkey 1693161fb3702830b586b2e36fa9ca4519f59f951b4Jeff Sharkey // Create a new emulated volume stacked above us, it will automatically 1703161fb3702830b586b2e36fa9ca4519f59f951b4Jeff Sharkey // be destroyed during unmount 1713161fb3702830b586b2e36fa9ca4519f59f951b4Jeff Sharkey std::string mediaPath(mPath + "/media"); 1723161fb3702830b586b2e36fa9ca4519f59f951b4Jeff Sharkey auto vol = std::shared_ptr<VolumeBase>( 1733161fb3702830b586b2e36fa9ca4519f59f951b4Jeff Sharkey new EmulatedVolume(mediaPath, mRawDevice, mFsUuid)); 1743161fb3702830b586b2e36fa9ca4519f59f951b4Jeff Sharkey addVolume(vol); 1753161fb3702830b586b2e36fa9ca4519f59f951b4Jeff Sharkey vol->create(); 1763161fb3702830b586b2e36fa9ca4519f59f951b4Jeff Sharkey 1779c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return OK; 1789c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} 1799c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 1809c48498f4529f623650c56d03e63324c8d813032Jeff Sharkeystatus_t PrivateVolume::doUnmount() { 1819c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey ForceUnmount(mPath); 1829c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 1839c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey if (TEMP_FAILURE_RETRY(rmdir(mPath.c_str()))) { 1849c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey PLOG(ERROR) << getId() << " failed to rmdir mount point " << mPath; 1859c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey } 1869c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 1879c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return OK; 1889c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} 1899c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 190d0640f6358041f7e2657167560b357078db73526Jeff Sharkeystatus_t PrivateVolume::doFormat(const std::string& fsType) { 191d0640f6358041f7e2657167560b357078db73526Jeff Sharkey std::string resolvedFsType = fsType; 192d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if (fsType == "auto") { 193d0640f6358041f7e2657167560b357078db73526Jeff Sharkey // For now, assume that all MMC devices are flash-based SD cards, and 194d0640f6358041f7e2657167560b357078db73526Jeff Sharkey // give everyone else ext4 because sysfs rotational isn't reliable. 195d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if ((major(mRawDevice) == kMajorBlockMmc) && f2fs::IsSupported()) { 196d0640f6358041f7e2657167560b357078db73526Jeff Sharkey resolvedFsType = "f2fs"; 197d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } else { 198d0640f6358041f7e2657167560b357078db73526Jeff Sharkey resolvedFsType = "ext4"; 199d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } 200d0640f6358041f7e2657167560b357078db73526Jeff Sharkey LOG(DEBUG) << "Resolved auto to " << resolvedFsType; 201d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } 202d0640f6358041f7e2657167560b357078db73526Jeff Sharkey 203d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if (resolvedFsType == "ext4") { 204d0640f6358041f7e2657167560b357078db73526Jeff Sharkey // TODO: change reported mountpoint once we have better selinux support 205d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if (ext4::Format(mDmDevPath, 0, "/data")) { 206d0640f6358041f7e2657167560b357078db73526Jeff Sharkey PLOG(ERROR) << getId() << " failed to format"; 207d0640f6358041f7e2657167560b357078db73526Jeff Sharkey return -EIO; 208d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } 209d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } else if (resolvedFsType == "f2fs") { 210d0640f6358041f7e2657167560b357078db73526Jeff Sharkey if (f2fs::Format(mDmDevPath)) { 211d0640f6358041f7e2657167560b357078db73526Jeff Sharkey PLOG(ERROR) << getId() << " failed to format"; 212d0640f6358041f7e2657167560b357078db73526Jeff Sharkey return -EIO; 213d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } 214d0640f6358041f7e2657167560b357078db73526Jeff Sharkey } else { 215d0640f6358041f7e2657167560b357078db73526Jeff Sharkey LOG(ERROR) << getId() << " unsupported filesystem " << fsType; 216d0640f6358041f7e2657167560b357078db73526Jeff Sharkey return -EINVAL; 2179c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey } 2189c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 2199c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey return OK; 2209c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} 2219c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey 2229c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} // namespace vold 2239c48498f4529f623650c56d03e63324c8d813032Jeff Sharkey} // namespace android 224