| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/download/ar_quick_look_tab_helper.h" |
| |
| #import <memory> |
| #import <string> |
| |
| #import "base/apple/foundation_util.h" |
| #import "base/files/file_path.h" |
| #import "base/files/file_util.h" |
| #import "base/functional/bind.h" |
| #import "base/metrics/histogram_functions.h" |
| #import "base/strings/escape.h" |
| #import "base/task/thread_pool.h" |
| #import "ios/chrome/browser/download/ar_quick_look_tab_helper_delegate.h" |
| #import "ios/chrome/browser/download/download_directory_util.h" |
| #import "ios/chrome/browser/download/mime_type_util.h" |
| #import "ios/web/public/download/download_task.h" |
| #import "net/base/mac/url_conversions.h" |
| #import "net/base/net_errors.h" |
| #import "net/base/url_util.h" |
| |
| const char kIOSDownloadARModelStateHistogram[] = |
| "Download.IOSDownloadARModelState"; |
| |
| const char kUsdzMimeTypeHistogramSuffix[] = ".USDZ"; |
| |
| namespace { |
| |
| // When an AR Quick Look URL contains this fragment, scaling the displayed |
| // image (e.g., by pinch-zooming) is disallowed. See |
| // https://developer.apple.com/videos/play/wwdc2019/612/ |
| const char kContentScalingKey[] = "allowsContentScaling"; |
| |
| // When an AR Quick Look URL contains this fragment, this URL will be used |
| // when users invoke the share sheet. See |
| // https://developer.apple.com/videos/play/wwdc2019/612/ |
| const char kCanonicalWebPageURLKey[] = "canonicalWebPageURL"; |
| |
| // Returns a suffix for Download.IOSDownloadARModelState histogram for the |
| // `download_task`. |
| std::string GetMimeTypeSuffix(web::DownloadTask* download_task) { |
| DCHECK(IsUsdzFileFormat(download_task->GetOriginalMimeType(), |
| download_task->GenerateFileName())); |
| return kUsdzMimeTypeHistogramSuffix; |
| } |
| |
| // Returns whether the `download_task` is complete or failed. |
| bool IsDownloadCompleteOrFailed(web::DownloadTask* download_task) { |
| switch (download_task->GetState()) { |
| case web::DownloadTask::State::kComplete: |
| case web::DownloadTask::State::kFailed: |
| case web::DownloadTask::State::kFailedNotResumable: |
| return YES; |
| default: |
| return NO; |
| } |
| } |
| |
| // Returns an enum for Download.IOSDownloadARModelState histogram for the |
| // terminated `download_task`. |
| IOSDownloadARModelState GetHistogramEnum(web::DownloadTask* download_task) { |
| DCHECK(download_task); |
| if (download_task->GetState() == web::DownloadTask::State::kNotStarted) { |
| return IOSDownloadARModelState::kCreated; |
| } |
| if (download_task->GetState() == web::DownloadTask::State::kInProgress) { |
| return IOSDownloadARModelState::kStarted; |
| } |
| DCHECK(download_task->IsDone()); |
| if (!IsUsdzFileFormat(download_task->GetMimeType(), |
| download_task->GenerateFileName())) { |
| return IOSDownloadARModelState::kWrongMimeTypeFailure; |
| } |
| if (download_task->GetHttpCode() == 401 || |
| download_task->GetHttpCode() == 403) { |
| return IOSDownloadARModelState::kUnauthorizedFailure; |
| } |
| if (download_task->GetErrorCode()) { |
| return IOSDownloadARModelState::kOtherFailure; |
| } |
| return IOSDownloadARModelState::kSuccessful; |
| } |
| |
| // Logs Download.IOSDownloadARModelState* histogram for the `download_task`. |
| void LogHistogram(web::DownloadTask* download_task) { |
| DCHECK(download_task); |
| base::UmaHistogramEnumeration( |
| kIOSDownloadARModelStateHistogram + GetMimeTypeSuffix(download_task), |
| GetHistogramEnum(download_task)); |
| } |
| |
| // Converts the ref of `url` into a query to allow parsing it using |
| // net::GetValueForKeyInQuery (as net does not provide utilities to |
| // parse ref). |
| GURL ConvertRefToQueryInUrl(const GURL& url) { |
| GURL::Replacements replacement; |
| replacement.SetQueryStr(url.ref_piece()); |
| replacement.ClearRef(); |
| |
| return url.ReplaceComponents(replacement); |
| } |
| |
| } // namespace |
| |
| ARQuickLookTabHelper::ARQuickLookTabHelper(web::WebState* web_state) |
| : web_state_(web_state) { |
| DCHECK(web_state_); |
| } |
| |
| ARQuickLookTabHelper::~ARQuickLookTabHelper() { |
| if (download_task_) { |
| RemoveCurrentDownload(); |
| } |
| } |
| |
| void ARQuickLookTabHelper::Download( |
| std::unique_ptr<web::DownloadTask> download_task) { |
| DCHECK(download_task); |
| if (download_task_) { |
| RemoveCurrentDownload(); |
| } |
| |
| base::FilePath download_dir; |
| if (!GetTempDownloadsDirectory(&download_dir)) { |
| return; |
| } |
| |
| // Take ownership of `download_task` and start the download. |
| download_task_ = std::move(download_task); |
| LogHistogram(download_task_.get()); |
| download_task_->AddObserver(this); |
| |
| download_task_->Start( |
| download_dir.Append(download_task_->GenerateFileName())); |
| |
| // Calling DownloadTask::Start() may cause the task to be immediately |
| // destroyed (e.g. if it is in error). Only call `LogHistogram` is it |
| // is still valid and owned by the current object. |
| if (download_task_) |
| LogHistogram(download_task_.get()); |
| } |
| |
| void ARQuickLookTabHelper::DidFinishDownload() { |
| DCHECK(IsDownloadCompleteOrFailed(download_task_.get())); |
| // Inform the delegate only if the download has been successful. |
| if (download_task_->GetHttpCode() == 401 || |
| download_task_->GetHttpCode() == 403 || download_task_->GetErrorCode() || |
| !IsUsdzFileFormat(download_task_->GetMimeType(), |
| download_task_->GenerateFileName())) { |
| return; |
| } |
| |
| GURL url = download_task_->GetOriginalUrl(); |
| if (url.SchemeIsBlob()) { |
| // If the download was a blob: URL, the task URL looks like the following |
| // "blob:https://example.com/...%23..." (i.e. the real URL but some of the |
| // characters such as `#` have been encoded). Extract the path from the |
| // blob: URL and decode it as a component to recreate the real URL. |
| // |
| // This is a hack as the https://www.w3.org/TR/FileAPI/#url seems to imply |
| // that the fragment needs to be stripped from the URL when creating a blob |
| // URL, but this appears to work well enough for https://crbug.com/1341660 |
| // issue. |
| url = GURL(base::UnescapeURLComponent( |
| url.path(), |
| base::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS)); |
| } |
| |
| // Convert the URL ref into query parameter to allow parsing of the URL |
| // ref using net::GetValueForKeyInQuery(...) (net doesn't provide a way |
| // to parse the ref). |
| url = ConvertRefToQueryInUrl(url); |
| |
| bool allow_content_scaling = true; |
| { |
| std::string key_value; |
| if (net::GetValueForKeyInQuery(url, kContentScalingKey, &key_value)) { |
| // Scaling is disabled if the value is set to 0. |
| allow_content_scaling = (key_value != "0"); |
| } |
| } |
| |
| NSURL* canonical_url = nil; |
| { |
| std::string key_value; |
| if (net::GetValueForKeyInQuery(url, kCanonicalWebPageURLKey, &key_value)) { |
| // Ignore extracted value if not a valid URL. |
| const GURL extracted_url(key_value); |
| if (extracted_url.is_valid()) { |
| canonical_url = net::NSURLWithGURL(extracted_url); |
| } |
| } |
| } |
| |
| NSURL* file_url = |
| base::apple::FilePathToNSURL(download_task_->GetResponsePath()); |
| [delegate_ presentUSDZFileWithURL:file_url |
| canonicalURL:canonical_url |
| webState:web_state_ |
| allowContentScaling:allow_content_scaling]; |
| } |
| |
| void ARQuickLookTabHelper::RemoveCurrentDownload() { |
| download_task_->RemoveObserver(this); |
| download_task_.reset(); |
| } |
| |
| void ARQuickLookTabHelper::OnDownloadUpdated(web::DownloadTask* download_task) { |
| DCHECK_EQ(download_task, download_task_.get()); |
| |
| switch (download_task_->GetState()) { |
| case web::DownloadTask::State::kCancelled: |
| LogHistogram(download_task_.get()); |
| RemoveCurrentDownload(); |
| break; |
| case web::DownloadTask::State::kInProgress: |
| // Do nothing. Histogram is already logged after the task was started. |
| break; |
| case web::DownloadTask::State::kComplete: |
| case web::DownloadTask::State::kFailed: |
| case web::DownloadTask::State::kFailedNotResumable: |
| LogHistogram(download_task_.get()); |
| DidFinishDownload(); |
| break; |
| case web::DownloadTask::State::kNotStarted: |
| NOTREACHED() << "Invalid state."; |
| } |
| } |
| |
| WEB_STATE_USER_DATA_KEY_IMPL(ARQuickLookTabHelper) |