Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2010 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | #define LOG_TAG "MtpCursor" |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 18 | |
Mike Lockwood | b14e588 | 2010-06-29 18:11:52 -0400 | [diff] [blame] | 19 | #include "MtpDebug.h" |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 20 | #include "MtpClient.h" |
| 21 | #include "MtpCursor.h" |
| 22 | #include "MtpDevice.h" |
| 23 | #include "MtpDeviceInfo.h" |
| 24 | #include "MtpObjectInfo.h" |
| 25 | #include "MtpStorageInfo.h" |
| 26 | |
Mike Lockwood | b14e588 | 2010-06-29 18:11:52 -0400 | [diff] [blame] | 27 | |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 28 | #include "binder/CursorWindow.h" |
| 29 | |
| 30 | namespace android { |
| 31 | |
| 32 | /* Device Column IDs */ |
Mike Lockwood | 0ef2bf5 | 2010-06-08 07:33:14 -0400 | [diff] [blame] | 33 | /* These must match the values in MtpCursor.java */ |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 34 | #define DEVICE_ROW_ID 1 |
| 35 | #define DEVICE_MANUFACTURER 2 |
| 36 | #define DEVICE_MODEL 3 |
| 37 | |
| 38 | /* Storage Column IDs */ |
Mike Lockwood | 0ef2bf5 | 2010-06-08 07:33:14 -0400 | [diff] [blame] | 39 | /* These must match the values in MtpCursor.java */ |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 40 | #define STORAGE_ROW_ID 101 |
| 41 | #define STORAGE_IDENTIFIER 102 |
| 42 | #define STORAGE_DESCRIPTION 103 |
| 43 | |
| 44 | /* Object Column IDs */ |
Mike Lockwood | 0ef2bf5 | 2010-06-08 07:33:14 -0400 | [diff] [blame] | 45 | /* These must match the values in MtpCursor.java */ |
| 46 | #define OBJECT_ROW_ID 201 |
| 47 | #define OBJECT_STORAGE_ID 202 |
| 48 | #define OBJECT_FORMAT 203 |
| 49 | #define OBJECT_PROTECTION_STATUS 204 |
| 50 | #define OBJECT_SIZE 205 |
| 51 | #define OBJECT_THUMB_FORMAT 206 |
| 52 | #define OBJECT_THUMB_SIZE 207 |
| 53 | #define OBJECT_THUMB_WIDTH 208 |
| 54 | #define OBJECT_THUMB_HEIGHT 209 |
| 55 | #define OBJECT_IMAGE_WIDTH 210 |
| 56 | #define OBJECT_IMAGE_HEIGHT 211 |
| 57 | #define OBJECT_IMAGE_DEPTH 212 |
| 58 | #define OBJECT_PARENT 213 |
| 59 | #define OBJECT_ASSOCIATION_TYPE 214 |
| 60 | #define OBJECT_ASSOCIATION_DESC 215 |
| 61 | #define OBJECT_SEQUENCE_NUMBER 216 |
| 62 | #define OBJECT_NAME 217 |
| 63 | #define OBJECT_DATE_CREATED 218 |
| 64 | #define OBJECT_DATE_MODIFIED 219 |
| 65 | #define OBJECT_KEYWORDS 220 |
Mike Lockwood | 3e072b3 | 2010-06-10 16:34:20 -0400 | [diff] [blame] | 66 | #define OBJECT_THUMB 221 |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 67 | |
| 68 | MtpCursor::MtpCursor(MtpClient* client, int queryType, int deviceID, |
Mike Lockwood | 4620df7 | 2010-09-23 14:01:11 -0400 | [diff] [blame] | 69 | MtpStorageID storageID, MtpObjectHandle objectID, |
| 70 | int columnCount, int* columns) |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 71 | : mClient(client), |
| 72 | mQueryType(queryType), |
| 73 | mDeviceID(deviceID), |
| 74 | mStorageID(storageID), |
| 75 | mQbjectID(objectID), |
| 76 | mColumnCount(columnCount), |
| 77 | mColumns(NULL) |
| 78 | { |
| 79 | if (columns) { |
| 80 | mColumns = new int[columnCount]; |
| 81 | memcpy(mColumns, columns, columnCount * sizeof(int)); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | MtpCursor::~MtpCursor() { |
| 86 | delete[] mColumns; |
| 87 | } |
| 88 | |
| 89 | int MtpCursor::fillWindow(CursorWindow* window, int startPos) { |
| 90 | LOGD("MtpCursor::fillWindow mQueryType: %d\n", mQueryType); |
| 91 | |
| 92 | switch (mQueryType) { |
| 93 | case DEVICE: |
| 94 | return fillDevices(window, startPos); |
| 95 | case DEVICE_ID: |
| 96 | return fillDevice(window, startPos); |
| 97 | case STORAGE: |
| 98 | return fillStorages(window, startPos); |
| 99 | case STORAGE_ID: |
| 100 | return fillStorage(window, startPos); |
| 101 | case OBJECT: |
| 102 | return fillObjects(window, 0, startPos); |
| 103 | case OBJECT_ID: |
| 104 | return fillObject(window, startPos); |
| 105 | case STORAGE_CHILDREN: |
| 106 | return fillObjects(window, -1, startPos); |
| 107 | case OBJECT_CHILDREN: |
| 108 | return fillObjects(window, mQbjectID, startPos); |
| 109 | default: |
| 110 | LOGE("MtpCursor::fillWindow: unknown query type %d\n", mQueryType); |
| 111 | return 0; |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | int MtpCursor::fillDevices(CursorWindow* window, int startPos) { |
| 116 | int count = 0; |
| 117 | MtpDeviceList& deviceList = mClient->getDeviceList(); |
| 118 | for (int i = 0; i < deviceList.size(); i++) { |
| 119 | MtpDevice* device = deviceList[i]; |
| 120 | if (fillDevice(window, device, startPos)) { |
| 121 | count++; |
| 122 | startPos++; |
| 123 | } else { |
| 124 | break; |
| 125 | } |
| 126 | } |
| 127 | return count; |
| 128 | } |
| 129 | |
| 130 | int MtpCursor::fillDevice(CursorWindow* window, int startPos) { |
| 131 | MtpDevice* device = mClient->getDevice(mDeviceID); |
| 132 | if (device && fillDevice(window, device, startPos)) |
| 133 | return 1; |
| 134 | else |
| 135 | return 0; |
| 136 | } |
| 137 | |
| 138 | int MtpCursor::fillStorages(CursorWindow* window, int startPos) { |
| 139 | int count = 0; |
| 140 | MtpDevice* device = mClient->getDevice(mDeviceID); |
| 141 | if (!device) |
| 142 | return 0; |
| 143 | MtpStorageIDList* storageIDs = device->getStorageIDs(); |
| 144 | if (!storageIDs) |
| 145 | return 0; |
| 146 | |
| 147 | for (int i = 0; i < storageIDs->size(); i++) { |
| 148 | MtpStorageID storageID = (*storageIDs)[i]; |
| 149 | if (fillStorage(window, device, storageID, startPos)) { |
| 150 | count++; |
| 151 | startPos++; |
| 152 | } else { |
| 153 | break; |
| 154 | } |
| 155 | } |
| 156 | delete storageIDs; |
| 157 | return count; |
| 158 | } |
| 159 | |
| 160 | int MtpCursor::fillStorage(CursorWindow* window, int startPos) { |
| 161 | MtpDevice* device = mClient->getDevice(mDeviceID); |
| 162 | if (device && fillStorage(window, device, mStorageID, startPos)) |
| 163 | return 1; |
| 164 | else |
| 165 | return 0; |
| 166 | } |
| 167 | |
| 168 | int MtpCursor::fillObjects(CursorWindow* window, int parent, int startPos) { |
| 169 | int count = 0; |
| 170 | MtpDevice* device = mClient->getDevice(mDeviceID); |
| 171 | if (!device) |
| 172 | return 0; |
| 173 | MtpObjectHandleList* handles = device->getObjectHandles(mStorageID, 0, parent); |
| 174 | if (!handles) |
| 175 | return 0; |
| 176 | |
| 177 | for (int i = 0; i < handles->size(); i++) { |
| 178 | MtpObjectHandle handle = (*handles)[i]; |
| 179 | if (fillObject(window, device, handle, startPos)) { |
| 180 | count++; |
| 181 | startPos++; |
| 182 | } else { |
| 183 | break; |
| 184 | } |
| 185 | } |
| 186 | delete handles; |
| 187 | return count; |
| 188 | } |
| 189 | |
| 190 | int MtpCursor::fillObject(CursorWindow* window, int startPos) { |
| 191 | MtpDevice* device = mClient->getDevice(mDeviceID); |
| 192 | if (device && fillObject(window, device, mQbjectID, startPos)) |
| 193 | return 1; |
| 194 | else |
| 195 | return 0; |
| 196 | } |
| 197 | |
| 198 | bool MtpCursor::fillDevice(CursorWindow* window, MtpDevice* device, int row) { |
| 199 | MtpDeviceInfo* deviceInfo = device->getDeviceInfo(); |
| 200 | if (!deviceInfo) |
| 201 | return false; |
| 202 | if (!prepareRow(window)) |
| 203 | return false; |
| 204 | |
| 205 | for (int i = 0; i < mColumnCount; i++) { |
| 206 | switch (mColumns[i]) { |
| 207 | case DEVICE_ROW_ID: |
| 208 | if (!putLong(window, device->getID(), row, i)) |
| 209 | return false; |
| 210 | break; |
| 211 | case DEVICE_MANUFACTURER: |
| 212 | if (!putString(window, deviceInfo->mManufacturer, row, i)) |
| 213 | return false; |
| 214 | break; |
| 215 | case DEVICE_MODEL: |
| 216 | if (!putString(window, deviceInfo->mModel, row, i)) |
| 217 | return false; |
| 218 | break; |
| 219 | default: |
| 220 | LOGE("fillDevice: unknown column %d\n", mColumns[i]); |
| 221 | return false; |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | return true; |
| 226 | } |
| 227 | |
| 228 | bool MtpCursor::fillStorage(CursorWindow* window, MtpDevice* device, |
| 229 | MtpStorageID storageID, int row) { |
| 230 | |
| 231 | LOGD("fillStorage %d\n", storageID); |
| 232 | |
| 233 | MtpStorageInfo* storageInfo = device->getStorageInfo(storageID); |
| 234 | if (!storageInfo) |
| 235 | return false; |
| 236 | if (!prepareRow(window)) { |
| 237 | delete storageInfo; |
| 238 | return false; |
| 239 | } |
| 240 | |
| 241 | const char* text; |
| 242 | for (int i = 0; i < mColumnCount; i++) { |
| 243 | switch (mColumns[i]) { |
| 244 | case STORAGE_ROW_ID: |
| 245 | if (!putLong(window, storageID, row, i)) |
| 246 | goto fail; |
| 247 | break; |
| 248 | case STORAGE_IDENTIFIER: |
| 249 | text = storageInfo->mVolumeIdentifier; |
| 250 | if (!text || !text[0]) |
| 251 | text = "Camera Storage"; |
| 252 | if (!putString(window, text, row, i)) |
| 253 | goto fail; |
| 254 | break; |
| 255 | case STORAGE_DESCRIPTION: |
| 256 | text = storageInfo->mStorageDescription; |
| 257 | if (!text || !text[0]) |
| 258 | text = "Storage Description"; |
| 259 | if (!putString(window, text, row, i)) |
| 260 | goto fail; |
| 261 | break; |
| 262 | default: |
| 263 | LOGE("fillStorage: unknown column %d\n", mColumns[i]); |
| 264 | goto fail; |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | delete storageInfo; |
| 269 | return true; |
| 270 | |
| 271 | fail: |
| 272 | delete storageInfo; |
| 273 | return false; |
| 274 | } |
| 275 | |
| 276 | bool MtpCursor::fillObject(CursorWindow* window, MtpDevice* device, |
| 277 | MtpObjectHandle objectID, int row) { |
| 278 | |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 279 | MtpObjectInfo* objectInfo = device->getObjectInfo(objectID); |
| 280 | if (!objectInfo) |
| 281 | return false; |
Mike Lockwood | 0ef2bf5 | 2010-06-08 07:33:14 -0400 | [diff] [blame] | 282 | // objectInfo->print(); |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 283 | if (!prepareRow(window)) { |
| 284 | delete objectInfo; |
| 285 | return false; |
| 286 | } |
| 287 | |
| 288 | for (int i = 0; i < mColumnCount; i++) { |
| 289 | switch (mColumns[i]) { |
| 290 | case OBJECT_ROW_ID: |
| 291 | if (!putLong(window, objectID, row, i)) |
| 292 | goto fail; |
| 293 | break; |
Mike Lockwood | 0ef2bf5 | 2010-06-08 07:33:14 -0400 | [diff] [blame] | 294 | case OBJECT_STORAGE_ID: |
| 295 | if (!putLong(window, objectInfo->mStorageID, row, i)) |
| 296 | goto fail; |
| 297 | break; |
| 298 | case OBJECT_FORMAT: |
| 299 | if (!putLong(window, objectInfo->mFormat, row, i)) |
| 300 | goto fail; |
| 301 | break; |
| 302 | case OBJECT_PROTECTION_STATUS: |
| 303 | if (!putLong(window, objectInfo->mProtectionStatus, row, i)) |
| 304 | goto fail; |
| 305 | break; |
| 306 | case OBJECT_SIZE: |
| 307 | if (!putLong(window, objectInfo->mCompressedSize, row, i)) |
| 308 | goto fail; |
| 309 | break; |
| 310 | case OBJECT_THUMB_FORMAT: |
| 311 | if (!putLong(window, objectInfo->mThumbFormat, row, i)) |
| 312 | goto fail; |
| 313 | break; |
| 314 | case OBJECT_THUMB_SIZE: |
| 315 | if (!putLong(window, objectInfo->mThumbCompressedSize, row, i)) |
| 316 | goto fail; |
| 317 | break; |
| 318 | case OBJECT_THUMB_WIDTH: |
| 319 | if (!putLong(window, objectInfo->mThumbPixWidth, row, i)) |
| 320 | goto fail; |
| 321 | break; |
| 322 | case OBJECT_THUMB_HEIGHT: |
| 323 | if (!putLong(window, objectInfo->mThumbPixHeight, row, i)) |
| 324 | goto fail; |
| 325 | break; |
| 326 | case OBJECT_IMAGE_WIDTH: |
| 327 | if (!putLong(window, objectInfo->mImagePixWidth, row, i)) |
| 328 | goto fail; |
| 329 | break; |
| 330 | case OBJECT_IMAGE_HEIGHT: |
| 331 | if (!putLong(window, objectInfo->mImagePixHeight, row, i)) |
| 332 | goto fail; |
| 333 | break; |
| 334 | case OBJECT_IMAGE_DEPTH: |
| 335 | if (!putLong(window, objectInfo->mImagePixDepth, row, i)) |
| 336 | goto fail; |
| 337 | break; |
| 338 | case OBJECT_PARENT: |
| 339 | if (!putLong(window, objectInfo->mParent, row, i)) |
| 340 | goto fail; |
| 341 | break; |
| 342 | case OBJECT_ASSOCIATION_TYPE: |
| 343 | if (!putLong(window, objectInfo->mAssociationType, row, i)) |
| 344 | goto fail; |
| 345 | break; |
| 346 | case OBJECT_ASSOCIATION_DESC: |
| 347 | if (!putLong(window, objectInfo->mAssociationDesc, row, i)) |
| 348 | goto fail; |
| 349 | break; |
| 350 | case OBJECT_SEQUENCE_NUMBER: |
| 351 | if (!putLong(window, objectInfo->mSequenceNumber, row, i)) |
| 352 | goto fail; |
| 353 | break; |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 354 | case OBJECT_NAME: |
| 355 | if (!putString(window, objectInfo->mName, row, i)) |
| 356 | goto fail; |
| 357 | break; |
Mike Lockwood | 0ef2bf5 | 2010-06-08 07:33:14 -0400 | [diff] [blame] | 358 | case OBJECT_DATE_CREATED: |
| 359 | if (!putLong(window, objectInfo->mDateCreated, row, i)) |
| 360 | goto fail; |
| 361 | break; |
| 362 | case OBJECT_DATE_MODIFIED: |
| 363 | if (!putLong(window, objectInfo->mDateModified, row, i)) |
| 364 | goto fail; |
| 365 | break; |
| 366 | case OBJECT_KEYWORDS: |
| 367 | if (!putString(window, objectInfo->mKeywords, row, i)) |
| 368 | goto fail; |
| 369 | break; |
Mike Lockwood | 3e072b3 | 2010-06-10 16:34:20 -0400 | [diff] [blame] | 370 | case OBJECT_THUMB: |
Mike Lockwood | 2b6c4a2 | 2010-07-26 20:35:21 -0400 | [diff] [blame] | 371 | if (!putThumbnail(window, objectID, objectInfo->mFormat, row, i)) |
Mike Lockwood | 3e072b3 | 2010-06-10 16:34:20 -0400 | [diff] [blame] | 372 | goto fail; |
| 373 | break; |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 374 | default: |
Mike Lockwood | 32eaa26 | 2010-09-20 18:23:21 -0400 | [diff] [blame] | 375 | LOGE("fillObject: unknown column %d\n", mColumns[i]); |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 376 | goto fail; |
| 377 | } |
| 378 | } |
| 379 | |
Mike Lockwood | 0ef2bf5 | 2010-06-08 07:33:14 -0400 | [diff] [blame] | 380 | delete objectInfo; |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 381 | return true; |
| 382 | |
| 383 | fail: |
| 384 | delete objectInfo; |
| 385 | return false; |
| 386 | } |
| 387 | |
| 388 | bool MtpCursor::prepareRow(CursorWindow* window) { |
| 389 | if (!window->setNumColumns(mColumnCount)) { |
| 390 | LOGE("Failed to change column count from %d to %d", window->getNumColumns(), mColumnCount); |
| 391 | return false; |
| 392 | } |
| 393 | field_slot_t * fieldDir = window->allocRow(); |
| 394 | if (!fieldDir) { |
| 395 | LOGE("Failed allocating fieldDir"); |
| 396 | return false; |
| 397 | } |
| 398 | return true; |
| 399 | } |
| 400 | |
| 401 | |
Mike Lockwood | 3a2f37f | 2010-09-28 09:14:50 -0400 | [diff] [blame] | 402 | bool MtpCursor::putLong(CursorWindow* window, int64_t value, int row, int column) { |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 403 | if (!window->putLong(row, column, value)) { |
| 404 | window->freeLastRow(); |
| 405 | LOGE("Failed allocating space for a long in column %d", column); |
| 406 | return false; |
| 407 | } |
| 408 | return true; |
| 409 | } |
| 410 | |
| 411 | bool MtpCursor::putString(CursorWindow* window, const char* text, int row, int column) { |
| 412 | int size = strlen(text) + 1; |
| 413 | int offset = window->alloc(size); |
| 414 | if (!offset) { |
| 415 | window->freeLastRow(); |
| 416 | LOGE("Failed allocating %u bytes for text/blob %s", size, text); |
| 417 | return false; |
| 418 | } |
| 419 | window->copyIn(offset, (const uint8_t*)text, size); |
| 420 | |
| 421 | // This must be updated after the call to alloc(), since that |
| 422 | // may move the field around in the window |
| 423 | field_slot_t * fieldSlot = window->getFieldSlot(row, column); |
| 424 | fieldSlot->type = FIELD_TYPE_STRING; |
| 425 | fieldSlot->data.buffer.offset = offset; |
| 426 | fieldSlot->data.buffer.size = size; |
| 427 | return true; |
| 428 | } |
| 429 | |
Mike Lockwood | 4620df7 | 2010-09-23 14:01:11 -0400 | [diff] [blame] | 430 | bool MtpCursor::putThumbnail(CursorWindow* window, MtpObjectHandle objectID, |
| 431 | MtpObjectFormat format, int row, int column) { |
Mike Lockwood | 3e072b3 | 2010-06-10 16:34:20 -0400 | [diff] [blame] | 432 | MtpDevice* device = mClient->getDevice(mDeviceID); |
Mike Lockwood | 2b6c4a2 | 2010-07-26 20:35:21 -0400 | [diff] [blame] | 433 | void* thumbnail; |
| 434 | int size, offset; |
| 435 | if (format == MTP_FORMAT_ASSOCIATION) { |
| 436 | thumbnail = NULL; |
| 437 | size = offset = 0; |
| 438 | } else { |
| 439 | thumbnail = device->getThumbnail(objectID, size); |
Mike Lockwood | 3e072b3 | 2010-06-10 16:34:20 -0400 | [diff] [blame] | 440 | |
Mike Lockwood | f43c641 | 2010-07-27 11:50:34 -0400 | [diff] [blame] | 441 | LOGV("putThumbnail: %p, size: %d\n", thumbnail, size); |
Mike Lockwood | 2b6c4a2 | 2010-07-26 20:35:21 -0400 | [diff] [blame] | 442 | offset = window->alloc(size); |
| 443 | if (!offset) { |
| 444 | window->freeLastRow(); |
| 445 | LOGE("Failed allocating %u bytes for thumbnail", size); |
| 446 | return false; |
| 447 | } |
Mike Lockwood | 3e072b3 | 2010-06-10 16:34:20 -0400 | [diff] [blame] | 448 | } |
Mike Lockwood | 2b6c4a2 | 2010-07-26 20:35:21 -0400 | [diff] [blame] | 449 | if (thumbnail) |
Mike Lockwood | 3e072b3 | 2010-06-10 16:34:20 -0400 | [diff] [blame] | 450 | window->copyIn(offset, (const uint8_t*)thumbnail, size); |
| 451 | |
| 452 | // This must be updated after the call to alloc(), since that |
| 453 | // may move the field around in the window |
| 454 | field_slot_t * fieldSlot = window->getFieldSlot(row, column); |
| 455 | fieldSlot->type = FIELD_TYPE_BLOB; |
| 456 | fieldSlot->data.buffer.offset = offset; |
| 457 | fieldSlot->data.buffer.size = size; |
| 458 | return true; |
| 459 | } |
| 460 | |
Mike Lockwood | 5ed68d2 | 2010-05-25 19:08:48 -0400 | [diff] [blame] | 461 | } // namespace android |