Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(62)

Side by Side Diff: content/browser/bluetooth/web_bluetooth_service_impl.cc

Issue 2466223002: Implement WebBluetooth getDescriptor[s] (Closed)
Patch Set: Implement WebBluetooth getDescriptor[s] Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 // ID Not In Map Note: 5 // ID Not In Map Note: A service, characteristic, or descriptor ID not in the
6 // A service, characteristic, or descriptor ID not in the corresponding 6 // corresponding WebBluetoothServiceImpl map [service_id_to_device_address_,
7 // WebBluetoothServiceImpl map [service_id_to_device_address_, 7 // characteristic_id_to_service_id_, descriptor_id_to_characteristic_id_]
8 // characteristic_id_to_service_id_, descriptor_to_characteristic_] implies a 8 // implies a hostile renderer because a renderer obtains the corresponding ID
9 // hostile renderer because a renderer obtains the corresponding ID from this 9 // from this class and it will be added to the map at that time.
10 // class and it will be added to the map at that time.
11 10
12 #include "content/browser/bluetooth/web_bluetooth_service_impl.h" 11 #include "content/browser/bluetooth/web_bluetooth_service_impl.h"
13 12
14 #include <algorithm> 13 #include <algorithm>
15 14
16 #include "base/strings/utf_string_conversions.h" 15 #include "base/strings/utf_string_conversions.h"
17 #include "base/threading/thread_task_runner_handle.h" 16 #include "base/threading/thread_task_runner_handle.h"
18 #include "content/browser/bluetooth/bluetooth_blocklist.h" 17 #include "content/browser/bluetooth/bluetooth_blocklist.h"
19 #include "content/browser/bluetooth/bluetooth_device_chooser_controller.h" 18 #include "content/browser/bluetooth/bluetooth_device_chooser_controller.h"
20 #include "content/browser/bluetooth/bluetooth_metrics.h" 19 #include "content/browser/bluetooth/bluetooth_metrics.h"
21 #include "content/browser/bluetooth/frame_connected_bluetooth_devices.h" 20 #include "content/browser/bluetooth/frame_connected_bluetooth_devices.h"
22 #include "content/browser/renderer_host/render_process_host_impl.h" 21 #include "content/browser/renderer_host/render_process_host_impl.h"
23 #include "content/common/bluetooth/web_bluetooth_device_id.h" 22 #include "content/common/bluetooth/web_bluetooth_device_id.h"
24 #include "content/public/browser/browser_thread.h" 23 #include "content/public/browser/browser_thread.h"
25 #include "content/public/browser/navigation_handle.h" 24 #include "content/public/browser/navigation_handle.h"
26 #include "content/public/browser/render_frame_host.h" 25 #include "content/public/browser/render_frame_host.h"
27 #include "content/public/browser/web_contents.h" 26 #include "content/public/browser/web_contents.h"
28 #include "device/bluetooth/bluetooth_adapter_factory_wrapper.h" 27 #include "device/bluetooth/bluetooth_adapter_factory_wrapper.h"
29 #include "device/bluetooth/bluetooth_remote_gatt_characteristic.h" 28 #include "device/bluetooth/bluetooth_remote_gatt_characteristic.h"
29 #include "device/bluetooth/bluetooth_remote_gatt_descriptor.h"
30 30
31 using device::BluetoothAdapterFactoryWrapper; 31 using device::BluetoothAdapterFactoryWrapper;
32 using device::BluetoothUUID; 32 using device::BluetoothUUID;
33 33
34 namespace content { 34 namespace content {
35 35
36 namespace { 36 namespace {
37 37
38 blink::mojom::WebBluetoothResult TranslateConnectErrorAndRecord( 38 blink::mojom::WebBluetoothResult TranslateConnectErrorAndRecord(
39 device::BluetoothDevice::ConnectErrorCode error_code) { 39 device::BluetoothDevice::ConnectErrorCode error_code) {
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after
122 UMAGATTOperationOutcome::NOT_PAIRED); 122 UMAGATTOperationOutcome::NOT_PAIRED);
123 return blink::mojom::WebBluetoothResult::GATT_NOT_PAIRED; 123 return blink::mojom::WebBluetoothResult::GATT_NOT_PAIRED;
124 case device::BluetoothRemoteGattService::GATT_ERROR_NOT_SUPPORTED: 124 case device::BluetoothRemoteGattService::GATT_ERROR_NOT_SUPPORTED:
125 RecordGATTOperationOutcome(operation, 125 RecordGATTOperationOutcome(operation,
126 UMAGATTOperationOutcome::NOT_SUPPORTED); 126 UMAGATTOperationOutcome::NOT_SUPPORTED);
127 return blink::mojom::WebBluetoothResult::GATT_NOT_SUPPORTED; 127 return blink::mojom::WebBluetoothResult::GATT_NOT_SUPPORTED;
128 } 128 }
129 NOTREACHED(); 129 NOTREACHED();
130 return blink::mojom::WebBluetoothResult::GATT_UNTRANSLATED_ERROR_CODE; 130 return blink::mojom::WebBluetoothResult::GATT_UNTRANSLATED_ERROR_CODE;
131 } 131 }
132 std::vector<device::BluetoothRemoteGattDescriptor*> GetDescriptorsByUUID(
scheib 2017/01/12 23:41:35 Empty line above this line. Also, ;) jun recently
dougt 2017/01/13 00:16:28 Done.
133 device::BluetoothRemoteGattCharacteristic* characteristic,
134 const BluetoothUUID& descriptor_uuid) {
135 std::vector<device::BluetoothRemoteGattDescriptor*> descriptors;
136 VLOG(1) << "Looking for descriptor: " << descriptor_uuid.canonical_value();
137 for (auto* descriptor : characteristic->GetDescriptors()) {
138 VLOG(1) << "Descriptor in cache: "
139 << descriptor->GetUUID().canonical_value();
140 if (descriptor->GetUUID() == descriptor_uuid) {
141 descriptors.push_back(descriptor);
142 }
143 }
144 return descriptors;
145 }
132 146
133 } // namespace 147 } // namespace
134 148
135 // Struct that holds the result of a cache query. 149 // Struct that holds the result of a cache query.
136 struct CacheQueryResult { 150 struct CacheQueryResult {
137 CacheQueryResult() : outcome(CacheQueryOutcome::SUCCESS) {} 151 CacheQueryResult() : outcome(CacheQueryOutcome::SUCCESS) {}
138 152
139 explicit CacheQueryResult(CacheQueryOutcome outcome) : outcome(outcome) {} 153 explicit CacheQueryResult(CacheQueryOutcome outcome) : outcome(outcome) {}
140 154
141 ~CacheQueryResult() {} 155 ~CacheQueryResult() {}
(...skipping 371 matching lines...) Expand 10 before | Expand all | Expand 10 after
513 RecordGetCharacteristicsOutcome( 527 RecordGetCharacteristicsOutcome(
514 quantity, characteristics_uuid 528 quantity, characteristics_uuid
515 ? UMAGetCharacteristicOutcome::NOT_FOUND 529 ? UMAGetCharacteristicOutcome::NOT_FOUND
516 : UMAGetCharacteristicOutcome::NO_CHARACTERISTICS); 530 : UMAGetCharacteristicOutcome::NO_CHARACTERISTICS);
517 callback.Run(characteristics_uuid 531 callback.Run(characteristics_uuid
518 ? blink::mojom::WebBluetoothResult::CHARACTERISTIC_NOT_FOUND 532 ? blink::mojom::WebBluetoothResult::CHARACTERISTIC_NOT_FOUND
519 : blink::mojom::WebBluetoothResult::NO_CHARACTERISTICS_FOUND, 533 : blink::mojom::WebBluetoothResult::NO_CHARACTERISTICS_FOUND,
520 base::nullopt /* characteristics */); 534 base::nullopt /* characteristics */);
521 } 535 }
522 536
537 void WebBluetoothServiceImpl::RemoteCharacteristicGetDescriptors(
538 const std::string& characteristic_instance_id,
539 blink::mojom::WebBluetoothGATTQueryQuantity quantity,
540 const base::Optional<BluetoothUUID>& descriptors_uuid,
541 const RemoteCharacteristicGetDescriptorsCallback& callback) {
542 DCHECK_CURRENTLY_ON(BrowserThread::UI);
543 if (descriptors_uuid &&
544 BluetoothBlocklist::Get().IsExcluded(descriptors_uuid.value())) {
545 callback.Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_DESCRIPTOR_UUID,
546 base::nullopt /* descriptor */);
547 return;
548 }
549
550 const CacheQueryResult query_result =
551 QueryCacheForCharacteristic(characteristic_instance_id);
552
553 if (query_result.outcome == CacheQueryOutcome::BAD_RENDERER) {
554 return;
555 }
556
557 if (query_result.outcome != CacheQueryOutcome::SUCCESS) {
558 callback.Run(query_result.GetWebResult(), base::nullopt /* descriptor */);
559 return;
560 }
561
562 auto descriptors = descriptors_uuid
563 ? GetDescriptorsByUUID(query_result.characteristic,
564 descriptors_uuid.value())
565 : query_result.characteristic->GetDescriptors();
566
567 std::vector<blink::mojom::WebBluetoothRemoteGATTDescriptorPtr>
568 response_descriptors;
569 for (device::BluetoothRemoteGattDescriptor* descriptor : descriptors) {
570 if (BluetoothBlocklist::Get().IsExcluded(descriptor->GetUUID())) {
571 continue;
572 }
573 std::string descriptor_instance_id = descriptor->GetIdentifier();
574 auto insert_result = descriptor_id_to_characteristic_id_.insert(
575 {descriptor_instance_id, characteristic_instance_id});
576 // If value is already in map, DCHECK it's valid.
577 if (!insert_result.second)
578 DCHECK(insert_result.first->second == characteristic_instance_id);
579
580 auto descriptor_ptr(blink::mojom::WebBluetoothRemoteGATTDescriptor::New());
581 descriptor_ptr->instance_id = descriptor_instance_id;
582 descriptor_ptr->uuid = descriptor->GetUUID().canonical_value();
583 response_descriptors.push_back(std::move(descriptor_ptr));
584
585 if (quantity == blink::mojom::WebBluetoothGATTQueryQuantity::SINGLE) {
586 break;
587 }
588 }
589
590 if (!response_descriptors.empty()) {
591 callback.Run(blink::mojom::WebBluetoothResult::SUCCESS,
592 std::move(response_descriptors));
593 return;
594 }
595
596 callback.Run(descriptors_uuid
597 ? blink::mojom::WebBluetoothResult::DESCRIPTOR_NOT_FOUND
598 : blink::mojom::WebBluetoothResult::NO_DESCRIPTORS_FOUND,
599 base::nullopt /* descriptors */);
600 }
601
523 void WebBluetoothServiceImpl::RemoteCharacteristicReadValue( 602 void WebBluetoothServiceImpl::RemoteCharacteristicReadValue(
524 const std::string& characteristic_instance_id, 603 const std::string& characteristic_instance_id,
525 const RemoteCharacteristicReadValueCallback& callback) { 604 const RemoteCharacteristicReadValueCallback& callback) {
526 DCHECK_CURRENTLY_ON(BrowserThread::UI); 605 DCHECK_CURRENTLY_ON(BrowserThread::UI);
527 RecordWebBluetoothFunctionCall( 606 RecordWebBluetoothFunctionCall(
528 UMAWebBluetoothFunction::CHARACTERISTIC_READ_VALUE); 607 UMAWebBluetoothFunction::CHARACTERISTIC_READ_VALUE);
529 608
530 const CacheQueryResult query_result = 609 const CacheQueryResult query_result =
531 QueryCacheForCharacteristic(characteristic_instance_id); 610 QueryCacheForCharacteristic(characteristic_instance_id);
532 611
533 if (query_result.outcome == CacheQueryOutcome::BAD_RENDERER) { 612 if (query_result.outcome == CacheQueryOutcome::BAD_RENDERER) {
534 return; 613 return;
535 } 614 }
536 615
537 if (query_result.outcome != CacheQueryOutcome::SUCCESS) { 616 if (query_result.outcome != CacheQueryOutcome::SUCCESS) {
538 RecordCharacteristicReadValueOutcome(query_result.outcome); 617 RecordCharacteristicReadValueOutcome(query_result.outcome);
539 callback.Run(query_result.GetWebResult(), base::nullopt /* value */); 618 callback.Run(query_result.GetWebResult(), base::nullopt /* value */);
540 return; 619 return;
541 } 620 }
542 621
543 if (BluetoothBlocklist::Get().IsExcludedFromReads( 622 if (BluetoothBlocklist::Get().IsExcludedFromReads(
544 query_result.characteristic->GetUUID())) { 623 query_result.characteristic->GetUUID())) {
545 RecordCharacteristicReadValueOutcome(UMAGATTOperationOutcome::BLOCKLISTED); 624 RecordCharacteristicReadValueOutcome(UMAGATTOperationOutcome::BLOCKLISTED);
546 callback.Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_READ, 625 callback.Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_READ,
547 base::nullopt /* value */); 626 base::nullopt /* value */);
548 return; 627 return;
549 } 628 }
550 629
551 query_result.characteristic->ReadRemoteCharacteristic( 630 query_result.characteristic->ReadRemoteCharacteristic(
552 base::Bind(&WebBluetoothServiceImpl::OnReadValueSuccess, 631 base::Bind(&WebBluetoothServiceImpl::OnCharacteristicReadValueSuccess,
553 weak_ptr_factory_.GetWeakPtr(), callback), 632 weak_ptr_factory_.GetWeakPtr(), callback),
554 base::Bind(&WebBluetoothServiceImpl::OnReadValueFailed, 633 base::Bind(&WebBluetoothServiceImpl::OnCharacteristicReadValueFailed,
555 weak_ptr_factory_.GetWeakPtr(), callback)); 634 weak_ptr_factory_.GetWeakPtr(), callback));
556 } 635 }
557 636
558 void WebBluetoothServiceImpl::RemoteCharacteristicWriteValue( 637 void WebBluetoothServiceImpl::RemoteCharacteristicWriteValue(
559 const std::string& characteristic_instance_id, 638 const std::string& characteristic_instance_id,
560 const std::vector<uint8_t>& value, 639 const std::vector<uint8_t>& value,
561 const RemoteCharacteristicWriteValueCallback& callback) { 640 const RemoteCharacteristicWriteValueCallback& callback) {
562 DCHECK_CURRENTLY_ON(BrowserThread::UI); 641 DCHECK_CURRENTLY_ON(BrowserThread::UI);
563 RecordWebBluetoothFunctionCall( 642 RecordWebBluetoothFunctionCall(
564 UMAWebBluetoothFunction::CHARACTERISTIC_WRITE_VALUE); 643 UMAWebBluetoothFunction::CHARACTERISTIC_WRITE_VALUE);
(...skipping 20 matching lines...) Expand all
585 } 664 }
586 665
587 if (BluetoothBlocklist::Get().IsExcludedFromWrites( 666 if (BluetoothBlocklist::Get().IsExcludedFromWrites(
588 query_result.characteristic->GetUUID())) { 667 query_result.characteristic->GetUUID())) {
589 RecordCharacteristicWriteValueOutcome(UMAGATTOperationOutcome::BLOCKLISTED); 668 RecordCharacteristicWriteValueOutcome(UMAGATTOperationOutcome::BLOCKLISTED);
590 callback.Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_WRITE); 669 callback.Run(blink::mojom::WebBluetoothResult::BLOCKLISTED_WRITE);
591 return; 670 return;
592 } 671 }
593 672
594 query_result.characteristic->WriteRemoteCharacteristic( 673 query_result.characteristic->WriteRemoteCharacteristic(
595 value, base::Bind(&WebBluetoothServiceImpl::OnWriteValueSuccess, 674 value,
596 weak_ptr_factory_.GetWeakPtr(), callback), 675 base::Bind(&WebBluetoothServiceImpl::OnCharacteristicWriteValueSuccess,
597 base::Bind(&WebBluetoothServiceImpl::OnWriteValueFailed, 676 weak_ptr_factory_.GetWeakPtr(), callback),
677 base::Bind(&WebBluetoothServiceImpl::OnCharacteristicWriteValueFailed,
598 weak_ptr_factory_.GetWeakPtr(), callback)); 678 weak_ptr_factory_.GetWeakPtr(), callback));
599 } 679 }
600 680
601 void WebBluetoothServiceImpl::RemoteCharacteristicStartNotifications( 681 void WebBluetoothServiceImpl::RemoteCharacteristicStartNotifications(
602 const std::string& characteristic_instance_id, 682 const std::string& characteristic_instance_id,
603 const RemoteCharacteristicStartNotificationsCallback& callback) { 683 const RemoteCharacteristicStartNotificationsCallback& callback) {
604 DCHECK_CURRENTLY_ON(BrowserThread::UI); 684 DCHECK_CURRENTLY_ON(BrowserThread::UI);
605 RecordWebBluetoothFunctionCall( 685 RecordWebBluetoothFunctionCall(
606 UMAWebBluetoothFunction::CHARACTERISTIC_START_NOTIFICATIONS); 686 UMAWebBluetoothFunction::CHARACTERISTIC_START_NOTIFICATIONS);
607 687
(...skipping 196 matching lines...) Expand 10 before | Expand all | Expand 10 after
804 884
805 void WebBluetoothServiceImpl::OnCreateGATTConnectionFailed( 885 void WebBluetoothServiceImpl::OnCreateGATTConnectionFailed(
806 base::TimeTicks start_time, 886 base::TimeTicks start_time,
807 const RemoteServerConnectCallback& callback, 887 const RemoteServerConnectCallback& callback,
808 device::BluetoothDevice::ConnectErrorCode error_code) { 888 device::BluetoothDevice::ConnectErrorCode error_code) {
809 DCHECK_CURRENTLY_ON(BrowserThread::UI); 889 DCHECK_CURRENTLY_ON(BrowserThread::UI);
810 RecordConnectGATTTimeFailed(base::TimeTicks::Now() - start_time); 890 RecordConnectGATTTimeFailed(base::TimeTicks::Now() - start_time);
811 callback.Run(TranslateConnectErrorAndRecord(error_code)); 891 callback.Run(TranslateConnectErrorAndRecord(error_code));
812 } 892 }
813 893
814 void WebBluetoothServiceImpl::OnReadValueSuccess( 894 void WebBluetoothServiceImpl::OnCharacteristicReadValueSuccess(
815 const RemoteCharacteristicReadValueCallback& callback, 895 const RemoteCharacteristicReadValueCallback& callback,
816 const std::vector<uint8_t>& value) { 896 const std::vector<uint8_t>& value) {
817 DCHECK_CURRENTLY_ON(BrowserThread::UI); 897 DCHECK_CURRENTLY_ON(BrowserThread::UI);
818 RecordCharacteristicReadValueOutcome(UMAGATTOperationOutcome::SUCCESS); 898 RecordCharacteristicReadValueOutcome(UMAGATTOperationOutcome::SUCCESS);
819 callback.Run(blink::mojom::WebBluetoothResult::SUCCESS, value); 899 callback.Run(blink::mojom::WebBluetoothResult::SUCCESS, value);
820 } 900 }
821 901
822 void WebBluetoothServiceImpl::OnReadValueFailed( 902 void WebBluetoothServiceImpl::OnCharacteristicReadValueFailed(
823 const RemoteCharacteristicReadValueCallback& callback, 903 const RemoteCharacteristicReadValueCallback& callback,
824 device::BluetoothRemoteGattService::GattErrorCode error_code) { 904 device::BluetoothRemoteGattService::GattErrorCode error_code) {
825 DCHECK_CURRENTLY_ON(BrowserThread::UI); 905 DCHECK_CURRENTLY_ON(BrowserThread::UI);
826 callback.Run(TranslateGATTErrorAndRecord( 906 callback.Run(TranslateGATTErrorAndRecord(
827 error_code, UMAGATTOperation::CHARACTERISTIC_READ), 907 error_code, UMAGATTOperation::CHARACTERISTIC_READ),
828 base::nullopt /* value */); 908 base::nullopt /* value */);
829 } 909 }
830 910
831 void WebBluetoothServiceImpl::OnWriteValueSuccess( 911 void WebBluetoothServiceImpl::OnCharacteristicWriteValueSuccess(
832 const RemoteCharacteristicWriteValueCallback& callback) { 912 const RemoteCharacteristicWriteValueCallback& callback) {
833 DCHECK_CURRENTLY_ON(BrowserThread::UI); 913 DCHECK_CURRENTLY_ON(BrowserThread::UI);
834 RecordCharacteristicWriteValueOutcome(UMAGATTOperationOutcome::SUCCESS); 914 RecordCharacteristicWriteValueOutcome(UMAGATTOperationOutcome::SUCCESS);
835 callback.Run(blink::mojom::WebBluetoothResult::SUCCESS); 915 callback.Run(blink::mojom::WebBluetoothResult::SUCCESS);
836 } 916 }
837 917
838 void WebBluetoothServiceImpl::OnWriteValueFailed( 918 void WebBluetoothServiceImpl::OnCharacteristicWriteValueFailed(
839 const RemoteCharacteristicWriteValueCallback& callback, 919 const RemoteCharacteristicWriteValueCallback& callback,
840 device::BluetoothRemoteGattService::GattErrorCode error_code) { 920 device::BluetoothRemoteGattService::GattErrorCode error_code) {
841 DCHECK_CURRENTLY_ON(BrowserThread::UI); 921 DCHECK_CURRENTLY_ON(BrowserThread::UI);
842 callback.Run(TranslateGATTErrorAndRecord( 922 callback.Run(TranslateGATTErrorAndRecord(
843 error_code, UMAGATTOperation::CHARACTERISTIC_WRITE)); 923 error_code, UMAGATTOperation::CHARACTERISTIC_WRITE));
844 } 924 }
845 925
846 void WebBluetoothServiceImpl::OnStartNotifySessionSuccess( 926 void WebBluetoothServiceImpl::OnStartNotifySessionSuccess(
847 const RemoteCharacteristicStartNotificationsCallback& callback, 927 const RemoteCharacteristicStartNotificationsCallback& callback,
848 std::unique_ptr<device::BluetoothGattNotifySession> notify_session) { 928 std::unique_ptr<device::BluetoothGattNotifySession> notify_session) {
(...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after
968 binding_.Close(); 1048 binding_.Close();
969 } 1049 }
970 1050
971 url::Origin WebBluetoothServiceImpl::GetOrigin() { 1051 url::Origin WebBluetoothServiceImpl::GetOrigin() {
972 return render_frame_host_->GetLastCommittedOrigin(); 1052 return render_frame_host_->GetLastCommittedOrigin();
973 } 1053 }
974 1054
975 void WebBluetoothServiceImpl::ClearState() { 1055 void WebBluetoothServiceImpl::ClearState() {
976 characteristic_id_to_notify_session_.clear(); 1056 characteristic_id_to_notify_session_.clear();
977 pending_primary_services_requests_.clear(); 1057 pending_primary_services_requests_.clear();
1058 descriptor_id_to_characteristic_id_.clear();
978 characteristic_id_to_service_id_.clear(); 1059 characteristic_id_to_service_id_.clear();
979 service_id_to_device_address_.clear(); 1060 service_id_to_device_address_.clear();
980 connected_devices_.reset( 1061 connected_devices_.reset(
981 new FrameConnectedBluetoothDevices(render_frame_host_)); 1062 new FrameConnectedBluetoothDevices(render_frame_host_));
982 allowed_devices_map_ = BluetoothAllowedDevicesMap(); 1063 allowed_devices_map_ = BluetoothAllowedDevicesMap();
983 device_chooser_controller_.reset(); 1064 device_chooser_controller_.reset();
984 BluetoothAdapterFactoryWrapper::Get().ReleaseAdapter(this); 1065 BluetoothAdapterFactoryWrapper::Get().ReleaseAdapter(this);
985 } 1066 }
986 1067
987 } // namespace content 1068 } // namespace content
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698