| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/metrics/window_configuration_recorder.h" |
| |
| #include "base/check.h" |
| #import "base/ios/ios_util.h" |
| #include "base/mac/foundation_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/timer/timer.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| @interface WindowConfigurationRecorder () |
| |
| // Called by the recording_timer_ to record current config. |
| - (void)recordConfiguration; |
| |
| @end |
| |
| namespace { |
| |
| // Delay between a recording of a new configuration. |
| static constexpr base::TimeDelta kRecordDelay = |
| base::TimeDelta::FromSeconds(20); |
| |
| // Timer callback for recording configuration after a delay. |
| void RecordWindowGeometryMetrics(WindowConfigurationRecorder* recorder) { |
| [recorder recordConfiguration]; |
| } |
| |
| // Returns all Foreground active windows that are Chrome windows. |
| NSArray<UIWindow*>* ForegroundWindowsForApplication( |
| UIApplication* application) { |
| NSMutableArray<UIWindow*>* windows = [NSMutableArray arrayWithCapacity:3]; |
| |
| if (base::ios::IsSceneStartupSupported()) { |
| if (@available(iOS 13, *)) { |
| for (UIScene* scene in application.connectedScenes) { |
| if (scene.activationState != UISceneActivationStateForegroundActive) |
| continue; |
| |
| UIWindowScene* windowScene = base::mac::ObjCCast<UIWindowScene>(scene); |
| for (UIWindow* window in windowScene.windows) { |
| // Skip other windows (like keyboard) that keep showing up. |
| if (![window isKindOfClass:NSClassFromString(@"ChromeOverlayWindow")]) |
| continue; |
| |
| [windows addObject:window]; |
| break; // Stop after one window per scene. This may be wrong. |
| } |
| } |
| } |
| } |
| return [windows copy]; |
| } |
| } // namespace |
| |
| @implementation WindowConfigurationRecorder { |
| // Repeating delay timer. |
| base::RepeatingTimer recording_timer_; |
| } |
| |
| - (instancetype)init { |
| if (self == [super init]) { |
| // When the app becomes active, set recording on. |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(applicationDidBecomeActive) |
| name:UIApplicationDidBecomeActiveNotification |
| object:nil]; |
| |
| // When the app resigns active, turn recording off. |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(applicationDidEnterBackground) |
| name:UIApplicationDidEnterBackgroundNotification |
| object:nil]; |
| |
| [self scheduleRecordConfiguration]; |
| } |
| return self; |
| } |
| |
| // Called when notified of UIApplicationDidBecomeActiveNotification. |
| - (void)applicationDidBecomeActive { |
| self.recording = YES; |
| [self scheduleRecordConfiguration]; |
| } |
| |
| // Called when notified of UIApplicationDidEnterBackgroundNotification |
| - (void)applicationDidEnterBackground { |
| self.recording = NO; |
| recording_timer_.Stop(); |
| } |
| |
| // Called to set or reset the timer. |
| - (void)scheduleRecordConfiguration { |
| if (recording_timer_.IsRunning()) { |
| recording_timer_.Reset(); |
| } else { |
| recording_timer_.Start( |
| FROM_HERE, kRecordDelay, |
| base::BindRepeating(&RecordWindowGeometryMetrics, self)); |
| } |
| } |
| |
| // Called when the timer actually fires. |
| - (void)recordConfiguration { |
| [self recordGeometryForScreen:[UIScreen mainScreen] |
| windows:ForegroundWindowsForApplication( |
| UIApplication.sharedApplication)]; |
| } |
| |
| // Computes configuration for given screen and windows and records it. |
| - (void)recordGeometryForScreen:(UIScreen*)screen |
| windows:(NSArray<UIWindow*>*)windows { |
| WindowConfiguration configuration = [self configurationForScreen:screen |
| windows:windows]; |
| base::UmaHistogramEnumeration("IOS.MultiWindow.Configuration", configuration); |
| } |
| |
| #pragma mark - Visible For Testing |
| |
| - (WindowConfiguration)configurationForScreen:(UIScreen*)screen |
| windows:(NSArray<UIWindow*>*)windows { |
| NSMutableArray<UIWindow*>* fullscreenWindows = [[NSMutableArray alloc] init]; |
| NSMutableArray<UIWindow*>* slideoverWindows = [[NSMutableArray alloc] init]; |
| NSMutableArray<UIWindow*>* sharedWindows = [[NSMutableArray alloc] init]; |
| |
| CGRect screenRect = screen.bounds; |
| for (UIWindow* window in windows) { |
| CGRect windowRect = window.frame; |
| |
| // Is the window full screen? |
| if (CGRectEqualToRect(screenRect, windowRect)) { |
| [fullscreenWindows addObject:window]; |
| continue; |
| } |
| // Is the window in slideover? Slideover windows are always both shorter |
| // and narrower than the screen. |
| if (screenRect.size.width > windowRect.size.width && |
| screenRect.size.height > windowRect.size.height) { |
| [slideoverWindows addObject:window]; |
| continue; |
| } |
| |
| // Otherwise, the window is shared. This shouldn't happen if there's |
| // a fullscreen window. |
| [sharedWindows addObject:window]; |
| } |
| |
| WindowConfiguration configuration = WindowConfiguration::kUnspecified; |
| |
| if (sharedWindows.count == 0) { |
| if (fullscreenWindows.count > 0) { |
| configuration = slideoverWindows.count > 0 |
| ? WindowConfiguration::kFullscreenWithSlideover |
| : WindowConfiguration::kFullscreen; |
| } else if (slideoverWindows.count > 0) { |
| configuration = WindowConfiguration::kSlideoverOnly; |
| } else { |
| // Configuration remains unspecificed -- were there no windows? |
| } |
| } else if (sharedWindows.count == 1) { |
| // Single Shared window cases. |
| UIUserInterfaceSizeClass sharedWindowSize = |
| sharedWindows[0].traitCollection.horizontalSizeClass; |
| if (sharedWindowSize == UIUserInterfaceSizeClassRegular) { |
| configuration = slideoverWindows.count > 0 |
| ? WindowConfiguration::kSharedStandardWithSlideover |
| : WindowConfiguration::kSharedStandard; |
| } else if (sharedWindowSize == UIUserInterfaceSizeClassCompact) { |
| configuration = slideoverWindows.count > 0 |
| ? WindowConfiguration::kSharedCompactWithSlideover |
| : WindowConfiguration::kSharedCompact; |
| } else { |
| // Configuration remains unspecified -- shared window has an unspecified |
| // size class. |
| } |
| } else if (sharedWindows.count == 2) { |
| UIUserInterfaceSizeClass firstWindowSize = |
| sharedWindows[0].traitCollection.horizontalSizeClass; |
| UIUserInterfaceSizeClass secondWindowSize = |
| sharedWindows[1].traitCollection.horizontalSizeClass; |
| |
| if (firstWindowSize == UIUserInterfaceSizeClassRegular && |
| secondWindowSize == UIUserInterfaceSizeClassRegular) { |
| configuration = |
| slideoverWindows.count > 0 |
| ? WindowConfiguration::kStandardBesideStandardWithSlideover |
| : WindowConfiguration::kStandardBesideStandard; |
| } else if (firstWindowSize == UIUserInterfaceSizeClassCompact && |
| secondWindowSize == UIUserInterfaceSizeClassCompact) { |
| configuration = |
| slideoverWindows.count > 0 |
| ? WindowConfiguration::kCompactBesideCompactWithSlideover |
| : WindowConfiguration::kCompactBesideCompact; |
| } else if (firstWindowSize != UIUserInterfaceSizeClassUnspecified && |
| secondWindowSize != UIUserInterfaceSizeClassUnspecified) { |
| // Since the sizes are neither both standard, nor both compact, nor is |
| // either of them unspecified, one must be standard and the other compact. |
| configuration = |
| slideoverWindows.count > 0 |
| ? WindowConfiguration::kStandardBesideCompactWithSlideover |
| : WindowConfiguration::kStandardBesideCompact; |
| } else { |
| // Configuration remains unspecified -- one of the two shared windows has |
| // an unspecified size class. |
| } |
| } else { |
| // Configuration remains unspecified -- more than two shared windows. |
| } |
| |
| return configuration; |
| } |
| |
| @end |