chromium/remoting/ios/app/physical_keyboard_detector.mm

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "remoting/ios/app/physical_keyboard_detector.h"

// A view to do measurement to figure out whether a physical keyboard is
// presented.
//
// The logic is hacky since iOS doesn't provide any API that tells you whether
// a physical keyboard is being used. We infer that in these steps:
//
//   1. Add a hidden text field (this view) to the view to be detected and
//      register the keyboardWillShow notification.
//   2. Make the text field first responder.
//   3. keyboardWillShow will get called. The keyboard's end frame will go
//      offscreen if the physical keyboard is presented.
//   4. Pass that information to the callback and remove the hidden text field.
//      The view will not flicker as long as we immediately remove the text field
//      in keyboardWillShow.
//
// Unfortunately there is no easy way to know immediately when the user connects
// or disconnects the keyboard.
@interface PhysicalKeyboardDetectorView : UITextField {
  void (^_callback)(BOOL);
}

- (void)detectOnView:(UIView*)view callback:(void (^)(BOOL))callback;

@end

@implementation PhysicalKeyboardDetectorView

- (instancetype)initWithFrame:(CGRect)frame {
  if ((self = [super initWithFrame:frame])) {
    self.hidden = YES;

    // This is to force keyboardWillShow to always be called.
    self.inputAccessoryView = [[UIView alloc] initWithFrame:CGRectZero];
  }
  return self;
}

- (void)dealloc {
  [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)detectOnView:(UIView*)view callback:(void (^)(BOOL))callback {
  _callback = callback;
  [view addSubview:self];
  [[NSNotificationCenter defaultCenter]
      addObserver:self
         selector:@selector(keyboardWillShow:)
             name:UIKeyboardWillShowNotification
           object:nil];
  [self becomeFirstResponder];
}

#pragma mark - Private

- (void)keyboardWillShow:(NSNotification*)notification {
  if (!self.isFirstResponder) {
    // Don't handle the notification if it is not for the detector view.
    return;
  }

  CGRect keyboardFrame = [(NSValue*)[notification.userInfo
      objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];

  // iPad will still show the toolbar at the top of the soft keyboard even if
  // the physical keyboard is presented, so the safer check is to see if the
  // bottom of the keyboard goes under the screen.
  int keyboardBottom = keyboardFrame.origin.y + keyboardFrame.size.height;
  BOOL isKeyboardOffScreen =
      keyboardBottom > UIScreen.mainScreen.bounds.size.height;
  [self resignFirstResponder];
  [self removeFromSuperview];
  _callback(isKeyboardOffScreen);
}

@end

#pragma mark - PhysicalKeyboardDetector

@implementation PhysicalKeyboardDetector

+ (void)detectOnView:(UIView*)view callback:(void (^)(BOOL))callback {
  PhysicalKeyboardDetectorView* detectorView =
      [[PhysicalKeyboardDetectorView alloc] initWithFrame:CGRectZero];
  [detectorView detectOnView:view callback:callback];
}

@end