/* * Copyright (C) 2020 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. */ #include "MultiDisplay.h" #include // for size_t #include // for max #include // for uint32_t #include // for operator<< #include // for set #include // for string, stoi #include // for unordered_map #include // for pair, make_pair #include // for vector #include "android/base/LayoutResolver.h" // for resolveLayout #include "android/base/Log.h" // for LogStreamVoi... #include "android/base/files/Stream.h" // for Stream #include "android/base/files/StreamSerializing.h" // for loadCollection #include "android/cmdline-option.h" // for android_cmdL... #include "android/emulation/MultiDisplayPipe.h" // for MultiDisplay... #include "android/emulation/control/adb/AdbInterface.h" // for AdbInterface #include "android/emulator-window.h" // for emulator_win... #include "android/featurecontrol/FeatureControl.h" // for isEnabled #include "android/featurecontrol/Features.h" // for MultiDisplay #include "android/globals.h" // for android_hw #include "android/hw-sensors.h" // for android_fold... #include "android/recording/screen-recorder.h" // for RecorderStates #include "android/skin/file.h" // for SkinLayout #include "android/skin/rect.h" // for SKIN_ROTATION_0 using android::base::AutoLock; namespace android { static MultiDisplay* sMultiDisplay = nullptr; MultiDisplay::MultiDisplay(const QAndroidEmulatorWindowAgent* const windowAgent, const QAndroidRecordScreenAgent* const recordAgent, bool isGuestMode) : mWindowAgent(windowAgent), mRecordAgent(recordAgent), mGuestMode(isGuestMode) { } //static MultiDisplay* MultiDisplay::getInstance() { return sMultiDisplay; } int MultiDisplay::setMultiDisplay(uint32_t id, int32_t x, int32_t y, uint32_t w, uint32_t h, uint32_t dpi, uint32_t flag, bool add) { int ret = 0; SkinRotation rotation = SKIN_ROTATION_0; LOG(VERBOSE) << "setMultiDisplay id " << id << " " << x << " " << y << " " << w << " " << h << " " << dpi << " " << flag << " " << (add? "add":"del"); if (!featurecontrol::isEnabled(android::featurecontrol::MultiDisplay)) { return -1; } if (android_foldable_any_folded_area_configured()) { return -1; } // TODO (wdu@) Remove this when multidisplay is supported by embedded // emulator. if (android_cmdLineOptions->qt_hide_window) { return -1; } if (mGuestMode) { return -1; } if (add && !multiDisplayParamValidate(id, w, h, dpi, flag)) { return -1; } // fetch rotation from EmulatorWindow // TODO: link to libui source code??? EmulatorWindow* window = emulator_window_get(); if (window) { SkinLayout* layout = emulator_window_get_layout(window); if (layout) { rotation = layout->orientation; } } if (rotation != SKIN_ROTATION_0) { mWindowAgent->showMessage("Please apply multiple displays without rotation", WINDOW_MESSAGE_ERROR, 1000); return -1; } if (add) { ret = createDisplay(&id); if (ret != 0) { return ret; } ret = setDisplayPose(id, x, y, w, h, dpi); if (ret != 0) { return ret; } } else { ret = destroyDisplay(id); if (ret != 0) { return ret; } } // Service in guest has already started through QemuMiscPipe when // bootCompleted. But this service may be killed, e.g., Android low // memory. Send broadcast again to guarantee servce running. // P.S. guest Service has check to avoid duplication. auto adbInterface = emulation::AdbInterface::getGlobal(); if (!adbInterface) { LOG(ERROR) << "Adb interface unavailable"; return -1; } adbInterface->enqueueCommand( {"shell", "am", "broadcast", "-a", "com.android.emulator.multidisplay.START", "-n", "com.android.emulator.multidisplay/" ".MultiDisplayServiceReceiver"}); MultiDisplayPipe* pipe = MultiDisplayPipe::getInstance(); if (pipe) { std::vector data; pipe->fillData(data, id, w, h, dpi, flag, add); LOG(VERBOSE) << "MultiDisplayPipe send " << (add ? "add":"del") << " id " << id << " width " << w << " height " << h << " dpi " << dpi << " flag " << flag; pipe->send(std::move(data)); } return 0; } bool MultiDisplay::getMultiDisplay(uint32_t id, int32_t* x, int32_t* y, uint32_t* w, uint32_t* h, uint32_t* dpi, uint32_t* flag, uint32_t* cb, bool* enabled) { AutoLock lock(mLock); if (mMultiDisplay.find(id) == mMultiDisplay.end()) { if (enabled) { *enabled = false; } return false; } if (x) { *x = mMultiDisplay[id].pos_x; } if (y) { *y = mMultiDisplay[id].pos_y; } if (w) { *w = mMultiDisplay[id].width; } if (h) { *h = mMultiDisplay[id].height; } if (dpi) { *dpi = mMultiDisplay[id].dpi; } if (flag) { *flag = mMultiDisplay[id].flag; } if (enabled) { *enabled = mMultiDisplay[id].enabled; } LOG(VERBOSE) << "getMultiDisplay " << id << "x " << mMultiDisplay[id].pos_x << " y " << mMultiDisplay[id].pos_y << " w " << mMultiDisplay[id].width << " h " << mMultiDisplay[id].height << " dpi " << mMultiDisplay[id].dpi << " flag " << mMultiDisplay[id].flag << " enable " << mMultiDisplay[id].enabled; return mMultiDisplay[id].enabled; } bool MultiDisplay::getNextMultiDisplay(int32_t start_id, uint32_t* id, int32_t* x, int32_t* y, uint32_t* w, uint32_t* h, uint32_t* dpi, uint32_t* flag, uint32_t* cb) { uint32_t key; std::map::iterator i; AutoLock lock(mLock); if (start_id < 0) { key = 0; } else { key = start_id + 1; } i = mMultiDisplay.lower_bound(key); if (i == mMultiDisplay.end()) { return false; } else { if (id) { *id = i->first; } if (x) { *x = i->second.pos_x; } if (y) { *y = i->second.pos_y; } if (w) { *w = i->second.width; } if (h) { *h = i->second.height; } if (dpi) { *dpi = i->second.dpi; } if (flag) { *flag = i->second.flag; } if (cb) { *cb = i->second.cb; } return true; } } bool MultiDisplay::translateCoordination(uint32_t* x, uint32_t* y, uint32_t* displayId) { if (mGuestMode) { *displayId = 0; return true; } AutoLock lock(mLock); uint32_t totalH, pos_x, pos_y, w, h; getCombinedDisplaySizeLocked(nullptr, &totalH); for (const auto iter : mMultiDisplay) { if (iter.first != 0 && iter.second.cb == 0) { continue; } // QT window uses the top left corner as the origin. // So we need to transform the (x, y) coordinates from // bottom left corner to top left corner. pos_x = iter.second.pos_x; pos_y = totalH - iter.second.height - iter.second.pos_y; w = iter.second.width; h = iter.second.height; if ((*x - pos_x) < w && (*y - pos_y) < h) { *x = *x - pos_x; *y = *y - pos_y; *displayId = iter.first; return true; } } return false; } void MultiDisplay::setGpuMode(bool isGuestMode, uint32_t w, uint32_t h) { mGuestMode = isGuestMode; if (isGuestMode) { // Guest mode will not start renderer, which in turn will not set the // default display from FrameBuffer. So we set display 0 here. AutoLock lock(mLock); mMultiDisplay.emplace(0, MultiDisplayInfo(0, 0, w, h, 0, 0, true, 0)); } } int MultiDisplay::createDisplay(uint32_t* displayId) { if (mGuestMode) { return -1; } if (displayId == nullptr) { LOG(ERROR) << "null displayId pointer"; return -1; } AutoLock lock(mLock); if (mMultiDisplay.size() > s_maxNumMultiDisplay) { LOG(ERROR) << "cannot create more displays, exceeding limits " << s_maxNumMultiDisplay; return -1; } if (mMultiDisplay.find(*displayId) != mMultiDisplay.end()) { return 0; } // displays created by internal rcCommands if (*displayId == s_invalidIdMultiDisplay) { for (int i = s_displayIdInternalBegin; i < s_maxNumMultiDisplay; i++) { if (mMultiDisplay.find(i) == mMultiDisplay.end()) { *displayId = i; break; } } } if (*displayId == s_invalidIdMultiDisplay) { LOG(ERROR) << "cannot create more internaldisplays, exceeding limits " << s_maxNumMultiDisplay - s_displayIdInternalBegin; return -1; } mMultiDisplay.emplace(*displayId, MultiDisplayInfo()); LOG(VERBOSE) << "create display " << *displayId; return 0; } int MultiDisplay::destroyDisplay(uint32_t displayId) { uint32_t width, height; bool needUIUpdate = false; bool restoreSkin = false; if (mGuestMode) { return -1; } { AutoLock lock(mLock); if (mMultiDisplay.find(displayId) == mMultiDisplay.end()) { return 0; } needUIUpdate = ((mMultiDisplay[displayId].cb != 0) ? true : false); mMultiDisplay.erase(displayId); if (needUIUpdate) { recomputeLayoutLocked(); getCombinedDisplaySizeLocked(&width, &height); if (getNumberActiveMultiDisplaysLocked() == 1) { // only display 0 remains, restore skin restoreSkin = true; } } } if (needUIUpdate) { // stop recording of this display if it is happening. RecorderStates states = mRecordAgent->getRecorderState(); if (states.displayId == displayId && states.state == RECORDER_RECORDING) { mRecordAgent->stopRecording(); } mWindowAgent->setUIDisplayRegion(0, 0, width, height); if (restoreSkin) { mWindowAgent->restoreSkin(); } } LOG(VERBOSE) << "delete display " << displayId; return 0; } int MultiDisplay::setDisplayPose(uint32_t displayId, int32_t x, int32_t y, uint32_t w, uint32_t h, uint32_t dpi) { bool UIUpdate = false; bool checkRecording = false; uint32_t width, height; if (mGuestMode) { return -1; } { AutoLock lock(mLock); if (mMultiDisplay.find(displayId) == mMultiDisplay.end()) { LOG(ERROR) << "cannot find display " << displayId; return -1; } if (mMultiDisplay[displayId].cb != 0 && (mMultiDisplay[displayId].width != w || mMultiDisplay[displayId].height != h)) { checkRecording = true; } mMultiDisplay[displayId].width = w; mMultiDisplay[displayId].height = h; mMultiDisplay[displayId].dpi = dpi; mMultiDisplay[displayId].pos_x = x; mMultiDisplay[displayId].pos_y = y; if (mMultiDisplay[displayId].cb != 0) { if (x == -1 && y == -1) { recomputeLayoutLocked(); } getCombinedDisplaySizeLocked(&width, &height); UIUpdate = true; } } if (checkRecording) { // stop recording of this display if it is happening. RecorderStates states = mRecordAgent->getRecorderState(); if (states.displayId == displayId && states.state == RECORDER_RECORDING) { mRecordAgent->stopRecording(); } } if (UIUpdate) { mWindowAgent->setUIDisplayRegion(0, 0, width, height); } LOG(VERBOSE) << "setDisplayPose " << displayId << " x " << x << " y " << y << " w " << w << " h " << h << " dpi " << dpi; return 0; } int MultiDisplay::getDisplayPose(uint32_t displayId, int32_t* x, int32_t* y, uint32_t* w, uint32_t* h) { if (mGuestMode) { return -1; } AutoLock lock(mLock); if (mMultiDisplay.find(displayId) == mMultiDisplay.end()) { LOG(ERROR) << "cannot find display " << displayId; return -1; } *x = mMultiDisplay[displayId].pos_x; *y = mMultiDisplay[displayId].pos_y; *w = mMultiDisplay[displayId].width; *h = mMultiDisplay[displayId].height; return 0; } int MultiDisplay::setDisplayColorBuffer(uint32_t displayId, uint32_t colorBuffer) { uint32_t width, height; bool noSkin = false; bool needUpdate = false; if (mGuestMode) { return -1; } { AutoLock lock(mLock); if (mMultiDisplay.find(displayId) == mMultiDisplay.end()) { LOG(ERROR) << "cannot find display" << displayId; return -1; } if (mMultiDisplay[displayId].cb == colorBuffer) { return 0; } if (mMultiDisplay[displayId].cb == 0) { mMultiDisplay[displayId].cb = colorBuffer; // first time cb assigned, update the UI needUpdate = true; recomputeLayoutLocked(); getCombinedDisplaySizeLocked(&width, &height); if (getNumberActiveMultiDisplaysLocked() == 2) { //disable skin when first display set, index 0 is the default one. noSkin = true; } } mMultiDisplay[displayId].cb = colorBuffer; } if (noSkin) { mWindowAgent->setNoSkin(); } if (needUpdate) { // Explicitly adjust host window size mWindowAgent->setUIDisplayRegion(0, 0, width, height); } LOG(VERBOSE) << "setDisplayColorBuffer " << displayId << " cb " << colorBuffer; return 0; } int MultiDisplay::getDisplayColorBuffer(uint32_t displayId, uint32_t* colorBuffer) { if (mGuestMode) { return -1; } AutoLock lock(mLock); if (mMultiDisplay.find(displayId) == mMultiDisplay.end()) { return -1; } *colorBuffer = mMultiDisplay[displayId].cb; return 0; } int MultiDisplay::getColorBufferDisplay(uint32_t colorBuffer, uint32_t* displayId) { if (mGuestMode) { return -1; } AutoLock lock(mLock); for (const auto& iter : mMultiDisplay) { if (iter.second.cb == colorBuffer) { *displayId = iter.first; return 0; } } return -1; } void MultiDisplay::getCombinedDisplaySize(uint32_t* w, uint32_t* h) { AutoLock lock(mLock); getCombinedDisplaySizeLocked(w, h); } void MultiDisplay::getCombinedDisplaySizeLocked(uint32_t* w, uint32_t* h) { uint32_t total_h = 0; uint32_t total_w = 0; for (const auto& iter : mMultiDisplay) { if (iter.first == 0 || iter.second.cb != 0) { total_h = std::max(total_h, iter.second.height + iter.second.pos_y); total_w = std::max(total_w, iter.second.width + iter.second.pos_x); } } if (h) *h = total_h; if (w) *w = total_w; } int MultiDisplay::getNumberActiveMultiDisplaysLocked() { int count = 0; for (const auto& iter : mMultiDisplay) { if (iter.first == 0 || iter.second.cb != 0) { count++; } } return count; } /* * Given that there are at most 11 displays, we can iterate through all possible * ways of showing each display in either the first row or the second row. It is * also possible to have an empty row. The best combination is to satisfy the * following two criteria: 1, The combined rectangle which contains all the * displays should have an aspect ratio that is close to the monitor's aspect * ratio. 2, The width of the first row should be close to the width of the * second row. * * Important detail of implementations: the x and y offsets saved in * mMultiDisplay use the bottom-left corner as origin. This coordinates will * be used by glviewport() in Postworker.cpp. However, the x and y offsets saved * by invoking setUIMultiDisplay() will be using top-left corner as origin. Thus, * input coordinates willl be calculated correctly when mouse events are * captured by QT window. * * TODO: We assume all displays pos_x/pos_y is adjustable here. This may * overwrite the specified pos_x/pos_y in setDisplayPos(); */ void MultiDisplay::recomputeLayoutLocked() { uint32_t monitorWidth, monitorHeight; double monitorAspectRatio = 1.0; if (!mWindowAgent->getMonitorRect(&monitorWidth, &monitorHeight)) { LOG(WARNING) << "Fail to get monitor width and height, use default ratio 1.0"; } else { monitorAspectRatio = (double) monitorHeight / (double) monitorWidth; } std::unordered_map> rectangles; for (const auto& iter : mMultiDisplay) { if (iter.first == 0 || iter.second.cb != 0) { rectangles[iter.first] = std::make_pair(iter.second.width, iter.second.height); } } for (const auto& iter : android::base::resolveLayout(rectangles, monitorAspectRatio)) { mMultiDisplay[iter.first].pos_x = iter.second.first; mMultiDisplay[iter.first].pos_y = iter.second.second; } } bool MultiDisplay::multiDisplayParamValidate(uint32_t id, uint32_t w, uint32_t h, uint32_t dpi, uint32_t flag) { // According the Android 9 CDD, // * 120 <= dpi <= 640 // * 320 * (dpi / 160) <= width // * 320 * (dpi / 160) <= height // * Screen aspect ratio cannot be longer (or wider) than 21:9 (or 9:21). // // Also we don't want a screen too big to limit the performance impact. // * 4K might be a good upper limit if (dpi < 120 || dpi > 640) { mWindowAgent->showMessage("dpi should be between 120 and 640", WINDOW_MESSAGE_ERROR, 1000); LOG(ERROR) << "dpi should be between 120 and 640"; return false; } if (w < 320 * dpi / 160 || h < 320 * dpi / 160) { mWindowAgent->showMessage("width and height should be >= 320dp", WINDOW_MESSAGE_ERROR, 1000); LOG(ERROR) << "width and height should be >= 320dp"; return false; } if (!((w <= 4096 && h <= 2160) || (w <= 2160 && h <= 4096))) { mWindowAgent->showMessage("resolution should not exceed 4k (4096*2160)", WINDOW_MESSAGE_ERROR, 1000); LOG(ERROR) << "resolution should not exceed 4k (4096*2160)"; return false; } if (w * 21 < h * 9 || w * 9 > h * 21) { mWindowAgent->showMessage("Aspect ratio cannot be longer (or wider) than 21:9 (or 9:21)", WINDOW_MESSAGE_ERROR, 1000); LOG(ERROR) << "Aspect ratio cannot be longer (or wider) than 21:9 (or 9:21)"; return false; } if (id > s_maxNumMultiDisplay) { mWindowAgent->showMessage("Display index cannot be more than 3", WINDOW_MESSAGE_ERROR, 1000); LOG(ERROR) << "Display index cannot be more than 3"; return false; } return true; } std::map MultiDisplay::parseConfig() { std::map ret; if (!android_cmdLineOptions || !android_cmdLineOptions->multidisplay) { return ret; } std::string s = android_cmdLineOptions->multidisplay; std::vector params; size_t last = 0, next = 0; while ((next = s.find(",", last)) != std::string::npos) { params.push_back(std::stoi(s.substr(last, next - last))); last = next + 1; } params.push_back(std::stoi(s.substr(last))); if (params.size() < 5 || params.size() % 5 != 0) { LOG(ERROR) << "Not enough parameters for multidisplay command"; return ret; } int i = 0; for (i = 0; i < params.size(); i+=5) { if (params[i] == 0 || params[i] > 3) { LOG(ERROR) << "multidisplay index should only be 1, 2, or 3"; ret.clear(); return ret; } if (multiDisplayParamValidate(params[i], params[i + 1], params[i + 2], params[i + 3], params[i + 4])) { LOG(ERROR) << "Invalid index/width/height/dpi settings for multidisplay command"; ret.clear(); return ret; } ret.emplace(params[i], MultiDisplayInfo(-1, -1, params[i + 1], params[i + 2], params[i + 3], params[i + 4], true)); } return ret; } void MultiDisplay::loadConfig() { // Get the multidisplay configs from startup parameters, if yes, // override the configs in config.ini // This stage happens before the MultiDisplayPipe created (bootCompleted) // or restored (snapshot). MultiDisplay configs will not send to guest // immediately. // For cold boot, MultiDisplayPipe queries configs when it is created. // For snapshot, MultiDisplayPipe query will not happen, instead, // onLoad() function later may overwrite the multidisplay states to // in sync with guest states. if (!featurecontrol::isEnabled(android::featurecontrol::MultiDisplay)) { return; } if (android_foldable_any_folded_area_configured()) { return; } if (mGuestMode) { return; } std::map info = parseConfig(); if (info.size()) { LOG(VERBOSE) << "config multidisplay with command-line"; for (const auto& i : info) { setMultiDisplay(i.first, -1, -1, i.second.width, i.second.height, i.second.dpi, i.second.flag, true); mWindowAgent->updateUIMultiDisplayPage(i.first); } } else { LOG(VERBOSE) << "config multidisplay with config.ini " << android_hw->hw_display1_width << "x" << android_hw->hw_display1_height << " " << android_hw->hw_display2_width << "x" << android_hw->hw_display2_height << " " << android_hw->hw_display3_width << "x" << android_hw->hw_display3_height; if (android_hw->hw_display1_width != 0 && android_hw->hw_display1_height != 0) { LOG(VERBOSE) << " add display 1"; setMultiDisplay(1, android_hw->hw_display1_xOffset, android_hw->hw_display1_yOffset, android_hw->hw_display1_width, android_hw->hw_display1_height, android_hw->hw_display1_density, android_hw->hw_display1_flag, true); mWindowAgent->updateUIMultiDisplayPage(1); } if (android_hw->hw_display2_width != 0 && android_hw->hw_display2_height != 0) { LOG(VERBOSE) << " add display 2"; setMultiDisplay(2, android_hw->hw_display2_xOffset, android_hw->hw_display2_yOffset, android_hw->hw_display2_width, android_hw->hw_display2_height, android_hw->hw_display2_density, android_hw->hw_display2_flag, true); mWindowAgent->updateUIMultiDisplayPage(2); } if (android_hw->hw_display3_width != 0 && android_hw->hw_display3_height != 0) { LOG(VERBOSE) << " add display 3"; setMultiDisplay(3, android_hw->hw_display3_xOffset, android_hw->hw_display3_yOffset, android_hw->hw_display3_width, android_hw->hw_display3_height, android_hw->hw_display3_density, android_hw->hw_display3_flag, true); mWindowAgent->updateUIMultiDisplayPage(3); } } } void MultiDisplay::onSave(base::Stream* stream) { AutoLock lock(mLock); base::saveCollection( stream, mMultiDisplay, [](base::Stream* s, const std::map::value_type& pair) { s->putBe32(pair.first); s->putBe32(pair.second.pos_x); s->putBe32(pair.second.pos_y); s->putBe32(pair.second.width); s->putBe32(pair.second.height); s->putBe32(pair.second.dpi); s->putBe32(pair.second.flag); s->putBe32(pair.second.cb); s->putByte(pair.second.enabled); }); } void MultiDisplay::onLoad(base::Stream* stream) { std::map displaysOnLoad; base::loadCollection(stream, &displaysOnLoad, [this](base::Stream* stream) -> std::map::value_type { const uint32_t idx = stream->getBe32(); const int32_t pos_x = stream->getBe32(); const int32_t pos_y = stream->getBe32(); const uint32_t width = stream->getBe32(); const uint32_t height = stream->getBe32(); const uint32_t dpi = stream->getBe32(); const uint32_t flag = stream->getBe32(); const uint32_t cb = stream->getBe32(); const bool enabled = stream->getByte(); return {idx, {pos_x, pos_y, width, height, dpi, flag, enabled, cb}}; }); // Restore the multidisplays of the snapshot. std::set ids; uint32_t combinedDisplayWidth = 0; uint32_t combinedDisplayHeight = 0; bool activeBeforeLoad, activeAfterLoad; { AutoLock lock(mLock); for (const auto& iter : mMultiDisplay) { ids.insert(iter.first); } for (const auto& iter: displaysOnLoad) { ids.insert(iter.first); } activeBeforeLoad = getNumberActiveMultiDisplaysLocked() > 1; mMultiDisplay.clear(); mMultiDisplay = displaysOnLoad; activeAfterLoad = getNumberActiveMultiDisplaysLocked() > 1; getCombinedDisplaySizeLocked(&combinedDisplayWidth, &combinedDisplayHeight); } if (activeAfterLoad) { if (!activeBeforeLoad) { mWindowAgent->setNoSkin(); } mWindowAgent->setUIDisplayRegion(0, 0, combinedDisplayWidth, combinedDisplayHeight); } else { if (activeBeforeLoad) { mWindowAgent->setUIDisplayRegion(0, 0, combinedDisplayWidth, combinedDisplayHeight); mWindowAgent->restoreSkin(); } } for (const auto& iter : ids) { mWindowAgent->updateUIMultiDisplayPage(iter); } } } // namespace android void android_init_multi_display(const QAndroidEmulatorWindowAgent* const windowAgent, const QAndroidRecordScreenAgent* const recordAgent, bool isGuestMode) { android::sMultiDisplay = new android::MultiDisplay(windowAgent, recordAgent, isGuestMode); } extern "C" { void android_load_multi_display_config() { if (!android::sMultiDisplay) { LOG(ERROR) << "Multidisplay not initiated yet, cannot config"; return; } android::sMultiDisplay->loadConfig(); } }