This question is part of this #3
Some Chromebooks are laptop and we can also turn it into tablet mode. See images from here
So my question is how to programmatically detect the mode(laptop or tablet) of Chromebook.
For this, I did this, but this is only working in Lenovo Flex11, in other Chromebook it is not working
context.getResources().getConfiguration().hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES;
if this condition returns true that means Chromebook is in tablet mode else in laptop mode
I need to check this because if the Chromebook is in laptop mode than I have to show a prediction bar only in particular Activity. If it is in tablet mode the soft keyboard will appear and the prediction bar is managed by candidate view of InputMethodService
We use a combination of two different things. The first is we detect if the device is in desktop mode using the following code snippet :
fun isDesktopMode() : Boolean {
var hasMouse = false
val hasKeyboard = resources.configuration.keyboard == KEYBOARD_QWERTY
val isKeyboardUseable = resources.configuration.hardKeyboardHidden == HARDKEYBOARDHIDDEN_NO
// Check each input device to see if it is a mouse
val inputManager = getSystemService(Context.INPUT_SERVICE) as InputManager
for (deviceId in inputManager.inputDeviceIds) {
val sourceMask = inputManager.getInputDevice(deviceId).sources
if ((sourceMask or SOURCE_MOUSE == sourceMask) ||
(sourceMask or SOURCE_TOUCHPAD == sourceMask) ||
(sourceMask or SOURCE_TRACKBALL == sourceMask)) {
hasMouse = true
}
}
return hasMouse && hasKeyboard && isKeyboardUseable
}
To detect if a keyboard is plugged in and/or available we use the following listener for listening to whether or not a keyboard has become active:
val inputManager = getSystemService(Context.INPUT_SERVICE) as InputManager
val inputDeviceListener = object: InputManager.InputDeviceListener {
override fun onInputDeviceRemoved(deviceId: Int) {
// Be careful checking device here as it is no longer attached to the system
}
override fun onInputDeviceAdded(deviceId: Int) {
// If you want to learn more about what has been attached, check the InputDevice
// using the id.
val sourceMask = inputManager.getInputDevice(deviceId).sources
if (sourceMask or SOURCE_KEYBOARD == sourceMask) {
//Keyboard has been Added
append_to_log("A keyboard has been added.")
}
}
override fun onInputDeviceChanged(deviceId: Int) {
// Best practice is to check for what you care about when things change (ie. are
// there any keyboards/mice still attached and usable. Users may have multiple
// devices attached (integrated keyboard a and bluetooth keyboard) and
// removing one does not mean the other is no longer available.
isInDesktopMode = isDesktopMode()
}
}
inputManager.registerInputDeviceListener(inputDeviceListener, null)
This post is licensed under Apache 2.0.
https://developers.google.com/open-source/devplat
Related
I need to check if the Android phone my app runs on is using casting which is enabled outside of my app.
It seems CastSession or SessionManager can provide the session related to my app which is not helpful for me.
For example, I can start casting with an app called xx which will cast or mirror the entire screen of my phone. Now, I need to notify when I open my app that the phone's screen is casting/mirroring so I can prevent showing specific content on my app.
I checked it with the code below:
val isCastingEnabledLiveData = MutableLiveData<Boolean>()
fun isCastingEnabled(context: Context): Boolean {
val mediaRouter = MediaRouter.getInstance(context)
if (mediaRouter.routes.size <= 1) {
isCastingEnabledLiveData.value = false
return
}
val selector = MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
.build()
mediaRouter.addCallback(selector, object : MediaRouter.Callback() {
override fun onRouteChanged(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
super.onRouteChanged(router, route)
isCastingEnabledLiveData.value = if (route != mediaRouter.defaultRoute) {
route?.connectionState != MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED
} else false
}
})
}
you can check whether the phone screen is casting or not by using the MediaRouter class.
Here is an example of how you could check if the phone screen is casting:
MediaRouter mediaRouter = (MediaRouter)
getSystemService(Context.MEDIA_ROUTER_SERVICE);
MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
if(route.isDefault()){
// Screen is not casting
} else {
// Screen is casting
}
This code uses the getSelectedRoute() method of the MediaRouter class to get the currently selected route. If the returned RouteInfo object is the default route, then the screen is not casting, otherwise it is.
Please note that this code uses the getSystemService(Context.MEDIA_ROUTER_SERVICE) method to get an instance of the MediaRouter class, so it should be called from an Activity or Service context.
Additionally, you could also use MediaRouter.Callback and MediaRouter.addCallback to set a callback to monitor the state of the casting, so you could get the updates on the casting state change as well.
Android 12 came up with a new Privacy Settings to disable access to the Camera and Mic sensors, which is referred as Toggles in the docs.
As it is mentioned in the docs:
the system reminds the user that the device-wide toggle is turned off
However, it seems that it only reminds the user when requesting the Camera permission and not when trying to authenticate the user using biometrics (face authentication on Pixel phones, which guess what!? It uses the camera). [I'm using AndroidX biometrics library]
Is there any way to find out if the Camera access has been blocked by the user without requesting any permission?
I guess the note in the docs didn't take into account that the app might use face authentication:
Note: The toggles mentioned in this section shouldn't require changes to your app's logic, as long as you follow privacy best practices.
Notes:
You can't register a new face in Settings when camera access is blocked. The Settings app does not show any error, just a blank camera feed
I am using Pixel 4 (Android 12)
The feature 'Join Wi-Fi by scanning a QR code' does not work and neither shows a feedback to the user if Camera access is blocked (Pixel 5)
So, I also looking for a solution - a have a biometric library and few reports appear in DM with the same problem - FaceUnlock doesn't work on Pixel 4 when the camera 'muted'
For now, still now fix, but maybe my research can help someone.
1. I checked the new API for PrivacyToggle's.
Android 12 introduces a new SensorPrivacyManager with supportsSensorToggle() method - it returns TRUE in case of device able to 'mute' camera or mic.
val sensorPrivacyManager = applicationContext
.getSystemService(SensorPrivacyManager::class.java)
as SensorPrivacyManager
val supportsMicrophoneToggle = sensorPrivacyManager
.supportsSensorToggle(Sensors.MICROPHONE)
val supportsCameraToggle = sensorPrivacyManager
.supportsSensorToggle(Sensors.CAMERA)
If you look into SensorPrivacyManager, you can find that it provides some more useful methods, so I develop the next code:
fun isCameraAccessible(): Boolean {
return !checkIsPrivacyToggled(SensorPrivacyManager.Sensors.CAMERA)
}
#SuppressLint("PrivateApi")
private fun checkIsPrivacyToggled(sensor: Int): Boolean {
val sensorPrivacyManager: SensorPrivacyManager =
appContext.getSystemService(SensorPrivacyManager::class.java)
if (sensorPrivacyManager.supportsSensorToggle(sensor)) {
val userHandleField = UserHandle::class.java.getDeclaredField("USER_CURRENT")
userHandleField.isAccessible = true
val userHandle = userHandleField.get(null) as Int
val m = SensorPrivacyManager::class.java.getDeclaredMethod(
"isSensorPrivacyEnabled",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType
)
m.isAccessible = true
return m.invoke(
sensorPrivacyManager,
sensor,
userHandle
) as Boolean
}
return false
}
Unfortunately, the service rejects this call due to SecurityException - missing android.permission.OBSERVE_SENSOR_PRIVACY, even if we declare it in Manifest.
At least on emulator.
2. We can try to identify a new "sensor-in-use" indicator
fun checkForIndicator(){
findViewById<View>(Window.ID_ANDROID_CONTENT)?.let {
it.setOnApplyWindowInsetsListener { view, windowInsets ->
val indicatorBounds = windowInsets.privacyIndicatorBounds
if(indicatorBounds !=null){
Toast.makeText(view.context, "Camera-in-use detected", Toast.LENGTH_LONG).show()
}
// change your UI to avoid overlapping
windowInsets
}
}
}
I didn't test this code (no real device), but as for me - it's not very useful, because we can check the camera indicator only AFTER we start Biometric Auth flow, when I need to understand is camera accessible BEFORE Biometric Auth started.
3. Because of PrivicyToogle related to QuickSettings, I decide that perhaps exists a way how Tiles determinate current Privacy Toggle state.
But this API use a very interesting solution - it does not use Settings.Global or Settings.Security section, instead, all preferences saved in "system/sensor_privacy.xml" and not accessible for 3rd party apps.
See SensorPrivacyService.java
I believe that exists a way how to find that Camera is blocked, but seems like some deeper research required
UPDATED 28/10/2021
So after some digging in AOSP sources, I found that APP_OP_CAMERA permission reflects the "blocking" state.
Just call if(SensorPrivacyCheck.isCameraBlocked()){ return } - this call also notify the system to show the "Unblock" dialog
Example
Solution:
#TargetApi(Build.VERSION_CODES.S)
#RestrictTo(RestrictTo.Scope.LIBRARY)
object SensorPrivacyCheck {
fun isMicrophoneBlocked(): Boolean {
return Utils.isAtLeastS && checkIsPrivacyToggled(SensorPrivacyManager.Sensors.MICROPHONE)
}
fun isCameraBlocked(): Boolean {
return Utils.isAtLeastS && checkIsPrivacyToggled(SensorPrivacyManager.Sensors.CAMERA)
}
#SuppressLint("PrivateApi", "BlockedPrivateApi")
private fun checkIsPrivacyToggled(sensor: Int): Boolean {
val sensorPrivacyManager: SensorPrivacyManager =
AndroidContext.appContext.getSystemService(SensorPrivacyManager::class.java)
if (sensorPrivacyManager.supportsSensorToggle(sensor)) {
try {
val permissionToOp: String =
AppOpCompatConstants.getAppOpFromPermission(
if (sensor == SensorPrivacyManager.Sensors.CAMERA)
Manifest.permission.CAMERA else Manifest.permission.RECORD_AUDIO
) ?: return false
val noteOp: Int = try {
AppOpsManagerCompat.noteOpNoThrow(
AndroidContext.appContext,
permissionToOp,
Process.myUid(),
AndroidContext.appContext.packageName
)
} catch (ignored: Throwable) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
PermissionUtils.appOpPermissionsCheckMiui(
permissionToOp,
Process.myUid(),
AndroidContext.appContext.packageName
) else AppOpsManagerCompat.MODE_IGNORED
}
return noteOp != AppOpsManagerCompat.MODE_ALLOWED
} catch (e: Throwable) {
e.printStackTrace()
}
}
return false
}
}
I have a test case where in the app "Set as default" prompt is opened. I want to test that with UI automator, and I had success with testing that case, but not 100% reliable. Unfortunately, some devices have "Set as default" prompt button written in caps, and some of those don't, so I'm not able to create 100% reliable tests for this test case. I have written this code below, but when fetching "Set as default" button by text, case of the letters don't play a role, but when I want to interact with that button, text case is important. Switching the IF-ELSE cases doesn't fix the problem in this case. And somehow, none of the dialog buttons ids work (button1, button2..) when I want to press those.
if (roleManager.isRoleAvailable(android.app.role.ASSISTANT)) {
if (!roleManager.isRoleHeld(android.app.role.ASSISTANT)) {
val myApp = device.findObject(UiSelector().textMatches(InstrumentationRegistry.getInstrumentation().targetContext.getString(R.string.app_name)))
myApp.click()
sleepLong()
var setAsDefaultButton: UiObject? = null
if (device.findObject(UiSelector().text("Set as default")) != null) {
setAsDefaultButton = device.findObject(UiSelector().text("Set as default"))
setAsDefaultButton?.click()
} else if (device.findObject(UiSelector().text("SET AS DEFAULT")) != null) {
setAsDefaultButton = device.findObject(UiSelector().text("SET AS DEFAULT"))
setAsDefaultButton?.click()
} else {
clickDialogPositiveButton()
}
}
}
You can use the Pattern object instead of using a string.
You can use in your code like:
val pattern = Pattern.compile("Set as default", Pattern.CASE_INSENSITIVE)
val setDefaultText = device.findObject(UiSelector().text(pattern))
if(setDefaultText != null)) {
setDefaultText.click()
} else {
clickDialogPositiveButton()
}
Based on the Jordan's example and hint, solution to this is to find an object with the Pattern. With pattern, you can search for UIObject with By.text(pattern). Take a note that the object found with the pattern needs to be UIObject2 instead of the UIObject.
val pattern = Pattern.compile("Set as the default", Pattern.CASE_INSENSITIVE)
if(device.findObject(UiSelector().text(pattern.toString())) != null) {
device.findObject(By.text(pattern)).click()
}
I am trying to get commas as the decimal separator for the numeric keyboard on a Samsung Galaxy Note II. I've tested my app on other devices (Moto X, rooted Samsung GS4) and their numpads have the correct separator if I change the language from the device's system settings. Is this feature not possible on Samsung's software?
Edit/Clarification: Is there a way to have the correct decimal separator without telling my users to download a different keyboard or creating my own input UI?
I experience the same problem, even now 2 years after you asked the question.
You better install the Google keyboard:
https://play.google.com/store/apps/details?id=com.google.android.inputmethod.latin&hl=no
If you need to distribute the APK using your internal device management system the APK can be downloaded from here:
https://www.apkmirror.com/apk/google-inc/google-keyboard/
Unfortunately (or not if you think security wise) there is no programmatic way for your app to decide which keyboard to use. Best option is to have a configuration page in your app telling the users how and why they should change their default keyboard.
You can use this code to open a keyboard settings dialog:
public void showInputMethodPicker() {
InputMethodManager imeManager = (InputMethodManager) getApplicationContext().getSystemService(INPUT_METHOD_SERVICE);
if (imeManager != null) {
imeManager.showInputMethodPicker();
}
}
I had a custom Entry, called LineEntry, this is my solution for the Samsumg devices to change the numeric keyboard.
[assembly: ExportRenderer(typeof(LineEntry), typeof(CustomEntryRenderer))]
namespace myApp.Droid.Services
{
public class CustomEntryRenderer : EntryRenderer
{
public CustomEntryRenderer(Android.Content.Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (e.OldElement != null || this.Element == null)
{
return;
}
//ClassNumber | NumberFlagDecimal | NumberFlagSigned
if (this.Control.InputType == Keyboard.Numeric.ToInputType())
{
// this.Control.KeyListener = Android.Text.Method.DigitsKeyListener.GetInstance(Resources.Configuration.Locale, true, true);
this.Control.KeyListener = Android.Text.Method.DigitsKeyListener.GetInstance(string.Format("1234567890{0}", System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator));
this.Control.InputType = Android.Text.InputTypes.ClassNumber | Android.Text.InputTypes.NumberFlagDecimal;
}
}
}
}
I'm building a mobile AIR app (Android & IOS) with Adobe Flash Builder 4.6 and I'm having this annoying problem.
Because I want to 'catch' the back-key on Android devices I added the following code to my main class:
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDown);
private function keyDown(k:KeyboardEvent):void {
if(k.keyCode == Keyboard.BACK) {
backClicked(); // function handling the back-action, not important
k.preventDefault();
}
Now somewhere else - nested in some classes - I've got a textfield:
TF = new TextField();
TF.type = TextFieldType.INPUT;
But when I set focus on the textfield the soft keyboard does appear, but I can't type a single character. When I disable the keylistener: no problem.
Seems like the listener is overriding my input field. Is there any workaround on this?
I have also implemented the back button functionality for my mobile apps , but i used to register keydown event only when my particular view is activated and removed the registered when view get deactivated.
in <s:view ....... viewActivate ="enableHardwareKeyListeners(event)" viewDeactivate="destroyHardwareKeyListeners(event)">
// add listener only for android device
if (Check for android device) {
NativeApplication.nativeApplication.addEventListener(KeyboardEvent.KEY_DOWN, handleHardwareKeysDown, false, 0);
NativeApplication.nativeApplication.addEventListener(KeyboardEvent.KEY_UP, handleHardwareKeysUp, false, 0);
this.setFocus();
}
private function destroyHardwareKeyListeners(event:ViewNavigatorEvent):void
{
if (NativeApplication.nativeApplication.hasEventListener(KeyboardEvent.KEY_DOWN))
NativeApplication.nativeApplication.removeEventListener(KeyboardEvent.KEY_DOWN, handleHardwareKeysDown);
if (NativeApplication.nativeApplication.hasEventListener(KeyboardEvent.KEY_UP))
NativeApplication.nativeApplication.removeEventListener(KeyboardEvent.KEY_UP, handleHardwareKeysUp);
}
private function handleHardwareKeysDown(e:KeyboardEvent):void
{
if (e.keyCode == Keyboard.BACK) {
e.preventDefault();
// your code
} else {
}
}
private function handleHardwareKeysUp(e:KeyboardEvent):void
{
if (e.keyCode == Keyboard.BACK)
e.preventDefault();
}
May this can help you.