diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java | 1641 |
1 files changed, 1641 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java new file mode 100644 index 0000000000..d0d77d6c49 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java @@ -0,0 +1,1641 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.display.DisplayManager; +import android.location.Criteria; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.media.AudioManager; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkInfo; +import android.os.Build; +import android.os.Bundle; +import android.os.Debug; +import android.os.LocaleList; +import android.os.Looper; +import android.os.PowerManager; +import android.os.Vibrator; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.Display; +import android.view.InputDevice; +import android.view.WindowManager; +import android.webkit.MimeTypeMap; +import androidx.annotation.Nullable; +import androidx.collection.SimpleArrayMap; +import androidx.core.content.res.ResourcesCompat; +import java.net.Proxy; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; +import org.jetbrains.annotations.NotNull; +import org.mozilla.gecko.annotation.RobocopTarget; +import org.mozilla.gecko.annotation.WrapForJNI; +import org.mozilla.gecko.util.HardwareCodecCapabilityUtils; +import org.mozilla.gecko.util.HardwareUtils; +import org.mozilla.gecko.util.InputDeviceUtils; +import org.mozilla.gecko.util.ProxySelector; +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.geckoview.BuildConfig; +import org.mozilla.geckoview.GeckoResult; +import org.mozilla.geckoview.R; + +public class GeckoAppShell { + private static final String LOGTAG = "GeckoAppShell"; + + /* + * Keep these values consistent with |SensorType| in HalSensor.h + */ + public static final int SENSOR_ORIENTATION = 0; + public static final int SENSOR_ACCELERATION = 1; + public static final int SENSOR_PROXIMITY = 2; + public static final int SENSOR_LINEAR_ACCELERATION = 3; + public static final int SENSOR_GYROSCOPE = 4; + public static final int SENSOR_LIGHT = 5; + public static final int SENSOR_ROTATION_VECTOR = 6; + public static final int SENSOR_GAME_ROTATION_VECTOR = 7; + + // We have static members only. + private GeckoAppShell() {} + + // Name for app-scoped prefs + public static final String APP_PREFS_NAME = "GeckoApp"; + + private static class GeckoCrashHandler extends CrashHandler { + + public GeckoCrashHandler(final Class<? extends Service> handlerService) { + super(handlerService); + } + + @Override + protected String getAppPackageName() { + final Context appContext = getAppContext(); + if (appContext == null) { + return "<unknown>"; + } + return appContext.getPackageName(); + } + + @Override + protected Context getAppContext() { + return getApplicationContext(); + } + + @Override + public boolean reportException(final Thread thread, final Throwable exc) { + try { + if (exc instanceof OutOfMemoryError) { + final SharedPreferences prefs = + getApplicationContext().getSharedPreferences(APP_PREFS_NAME, 0); + final SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(PREFS_OOM_EXCEPTION, true); + + // Synchronously write to disk so we know it's done before we + // shutdown + editor.commit(); + } + + reportJavaCrash(exc, getExceptionStackTrace(exc)); + + } catch (final Throwable e) { + } + + // reportJavaCrash should have caused us to hard crash. If we're still here, + // it probably means Gecko is not loaded, and we should do something else. + if (BuildConfig.MOZ_CRASHREPORTER && BuildConfig.MOZILLA_OFFICIAL) { + // Only use Java crash reporter if enabled on official build. + return super.reportException(thread, exc); + } + return false; + } + } + + private static String sAppNotes; + private static CrashHandler sCrashHandler; + + public static synchronized CrashHandler ensureCrashHandling( + final Class<? extends Service> handler) { + if (sCrashHandler == null) { + sCrashHandler = new GeckoCrashHandler(handler); + } + + return sCrashHandler; + } + + private static Class<? extends Service> sCrashHandlerService; + + public static synchronized void setCrashHandlerService( + final Class<? extends Service> handlerService) { + sCrashHandlerService = handlerService; + } + + public static synchronized Class<? extends Service> getCrashHandlerService() { + return sCrashHandlerService; + } + + @WrapForJNI(exceptionMode = "ignore") + /* package */ static synchronized String getAppNotes() { + return sAppNotes; + } + + public static synchronized void appendAppNotesToCrashReport(final String notes) { + if (sAppNotes == null) { + sAppNotes = notes; + } else { + sAppNotes += '\n' + notes; + } + } + + private static volatile boolean locationHighAccuracyEnabled; + private static volatile boolean locationListeningRequested = false; + private static volatile boolean locationPaused = false; + + // See also HardwareUtils.LOW_MEMORY_THRESHOLD_MB. + private static final int HIGH_MEMORY_DEVICE_THRESHOLD_MB = 768; + + private static int sDensityDpi; + private static Float sDensity; + private static int sScreenDepth; + private static boolean sUseMaxScreenDepth; + private static Float sScreenRefreshRate; + + /* Is the value in sVibrationEndTime valid? */ + private static boolean sVibrationMaybePlaying; + + /* Time (in System.nanoTime() units) when the currently-playing vibration + * is scheduled to end. This value is valid only when + * sVibrationMaybePlaying is true. */ + private static long sVibrationEndTime; + + private static Sensor gAccelerometerSensor; + private static Sensor gLinearAccelerometerSensor; + private static Sensor gGyroscopeSensor; + private static Sensor gOrientationSensor; + private static Sensor gLightSensor; + private static Sensor gRotationVectorSensor; + private static Sensor gGameRotationVectorSensor; + + /* + * Keep in sync with constants found here: + * http://searchfox.org/mozilla-central/source/uriloader/base/nsIWebProgressListener.idl + */ + public static final int WPL_STATE_START = 0x00000001; + public static final int WPL_STATE_STOP = 0x00000010; + public static final int WPL_STATE_IS_DOCUMENT = 0x00020000; + public static final int WPL_STATE_IS_NETWORK = 0x00040000; + + /* Keep in sync with constants found here: + http://searchfox.org/mozilla-central/source/netwerk/base/nsINetworkLinkService.idl + */ + public static final int LINK_TYPE_UNKNOWN = 0; + public static final int LINK_TYPE_ETHERNET = 1; + public static final int LINK_TYPE_USB = 2; + public static final int LINK_TYPE_WIFI = 3; + public static final int LINK_TYPE_WIMAX = 4; + public static final int LINK_TYPE_MOBILE = 9; + + public static final String PREFS_OOM_EXCEPTION = "OOMException"; + + /* The Android-side API: API methods that Android calls */ + + // helper methods + @WrapForJNI + /* package */ static native void reportJavaCrash(Throwable exc, String stackTrace); + + private static Rect sScreenSizeOverride; + + @WrapForJNI(stubName = "NotifyObservers", dispatchTo = "gecko") + private static native void nativeNotifyObservers(String topic, String data); + + @WrapForJNI(stubName = "AppendAppNotesToCrashReport", dispatchTo = "gecko") + public static native void nativeAppendAppNotesToCrashReport(final String notes); + + @RobocopTarget + public static void notifyObservers(final String topic, final String data) { + notifyObservers(topic, data, GeckoThread.State.RUNNING); + } + + public static void notifyObservers( + final String topic, final String data, final GeckoThread.State state) { + if (GeckoThread.isStateAtLeast(state)) { + nativeNotifyObservers(topic, data); + } else { + GeckoThread.queueNativeCallUntil( + state, + GeckoAppShell.class, + "nativeNotifyObservers", + String.class, + topic, + String.class, + data); + } + } + + /* + * The Gecko-side API: API methods that Gecko calls + */ + + @WrapForJNI(exceptionMode = "ignore") + private static String getExceptionStackTrace(final Throwable e) { + return CrashHandler.getExceptionStackTrace(CrashHandler.getRootException(e)); + } + + @WrapForJNI(exceptionMode = "ignore") + private static synchronized void handleUncaughtException(final Throwable e) { + if (sCrashHandler != null) { + sCrashHandler.uncaughtException(null, e); + } + } + + private static float getLocationAccuracy(final Location location) { + final float radius = location.getAccuracy(); + return (location.hasAccuracy() && radius > 0) ? radius : 1001; + } + + private static Location determineReliableLocation( + @NotNull final Location locA, @NotNull final Location locB) { + // The 6 seconds were chosen arbitrarily + final long closeTime = 6000000000L; + final boolean isNearSameTime = + Math.abs((locA.getElapsedRealtimeNanos() - locB.getElapsedRealtimeNanos())) <= closeTime; + final boolean isAMoreAccurate = getLocationAccuracy(locA) < getLocationAccuracy(locB); + final boolean isAMoreRecent = locA.getElapsedRealtimeNanos() > locB.getElapsedRealtimeNanos(); + if (isNearSameTime) { + return isAMoreAccurate ? locA : locB; + } + return isAMoreRecent ? locA : locB; + } + + // Permissions are explicitly checked when requesting content permission. + @SuppressLint("MissingPermission") + private static @Nullable Location getLastKnownLocation(final LocationManager lm) { + Location lastKnownLocation = null; + final List<String> providers = lm.getAllProviders(); + + for (final String provider : providers) { + final Location location = lm.getLastKnownLocation(provider); + if (location == null) { + continue; + } + + if (lastKnownLocation == null) { + lastKnownLocation = location; + continue; + } + lastKnownLocation = determineReliableLocation(lastKnownLocation, location); + } + return lastKnownLocation; + } + + // Toggles the location listeners on/off, which will then provide/stop location information + @WrapForJNI(calledFrom = "gecko") + private static synchronized boolean enableLocationUpdates(final boolean enable) { + locationListeningRequested = enable; + final boolean canListen = updateLocationListeners(); + if (!canListen && locationListeningRequested) { + // Didn't successfully start listener when requested + locationListeningRequested = false; + } + return canListen; + } + + // Permissions are explicitly checked when requesting content permission. + @SuppressLint("MissingPermission") + private static synchronized boolean updateLocationListeners() { + final boolean shouldListen = locationListeningRequested && !locationPaused; + final LocationManager lm = getLocationManager(getApplicationContext()); + if (lm == null) { + return false; + } + + if (!shouldListen) { + // Could not complete request, because paused + if (locationListeningRequested) { + return false; + } + lm.removeUpdates(sAndroidListeners); + return true; + } + + if (!lm.isProviderEnabled(LocationManager.GPS_PROVIDER) + && !lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { + return false; + } + + final Location lastKnownLocation = getLastKnownLocation(lm); + if (lastKnownLocation != null) { + sAndroidListeners.onLocationChanged(lastKnownLocation); + } + + final Criteria criteria = new Criteria(); + criteria.setSpeedRequired(false); + criteria.setBearingRequired(false); + criteria.setAltitudeRequired(false); + if (locationHighAccuracyEnabled) { + criteria.setAccuracy(Criteria.ACCURACY_FINE); + } else { + criteria.setAccuracy(Criteria.ACCURACY_COARSE); + } + + final String provider = lm.getBestProvider(criteria, true); + if (provider == null) { + return false; + } + + final Looper l = Looper.getMainLooper(); + lm.requestLocationUpdates(provider, 100, 0.5f, sAndroidListeners, l); + return true; + } + + public static void pauseLocation() { + locationPaused = true; + updateLocationListeners(); + } + + public static void resumeLocation() { + locationPaused = false; + updateLocationListeners(); + } + + private static LocationManager getLocationManager(final Context context) { + try { + return (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + } catch (final NoSuchFieldError e) { + // Some Tegras throw exceptions about missing the CONTROL_LOCATION_UPDATES permission, + // which allows enabling/disabling location update notifications from the cell radio. + // CONTROL_LOCATION_UPDATES is not for use by normal applications, but we might be + // hitting this problem if the Tegras are confused about missing cell radios. + Log.e(LOGTAG, "LOCATION_SERVICE not found?!", e); + return null; + } + } + + @WrapForJNI(calledFrom = "gecko") + private static void enableLocationHighAccuracy(final boolean enable) { + locationHighAccuracyEnabled = enable; + } + + @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") + /* package */ static native void onSensorChanged( + int halType, float x, float y, float z, float w, long time); + + @WrapForJNI(calledFrom = "any", dispatchTo = "gecko") + /* package */ static native void onLocationChanged( + double latitude, + double longitude, + double altitude, + float accuracy, + float altitudeAccuracy, + float heading, + float speed); + + private static class AndroidListeners implements SensorEventListener, LocationListener { + @Override + public void onAccuracyChanged(final Sensor sensor, final int accuracy) {} + + @Override + public void onSensorChanged(final SensorEvent s) { + final int sensorType = s.sensor.getType(); + int halType = 0; + float x = 0.0f, y = 0.0f, z = 0.0f, w = 0.0f; + // SensorEvent timestamp is in nanoseconds, Gecko expects microseconds. + final long time = s.timestamp / 1000; + + switch (sensorType) { + case Sensor.TYPE_ACCELEROMETER: + case Sensor.TYPE_LINEAR_ACCELERATION: + case Sensor.TYPE_ORIENTATION: + if (sensorType == Sensor.TYPE_ACCELEROMETER) { + halType = SENSOR_ACCELERATION; + } else if (sensorType == Sensor.TYPE_LINEAR_ACCELERATION) { + halType = SENSOR_LINEAR_ACCELERATION; + } else { + halType = SENSOR_ORIENTATION; + } + x = s.values[0]; + y = s.values[1]; + z = s.values[2]; + break; + + case Sensor.TYPE_GYROSCOPE: + halType = SENSOR_GYROSCOPE; + x = (float) Math.toDegrees(s.values[0]); + y = (float) Math.toDegrees(s.values[1]); + z = (float) Math.toDegrees(s.values[2]); + break; + + case Sensor.TYPE_LIGHT: + halType = SENSOR_LIGHT; + x = s.values[0]; + break; + + case Sensor.TYPE_ROTATION_VECTOR: + case Sensor.TYPE_GAME_ROTATION_VECTOR: // API >= 18 + halType = + (sensorType == Sensor.TYPE_ROTATION_VECTOR + ? SENSOR_ROTATION_VECTOR + : SENSOR_GAME_ROTATION_VECTOR); + x = s.values[0]; + y = s.values[1]; + z = s.values[2]; + if (s.values.length >= 4) { + w = s.values[3]; + } else { + // s.values[3] was optional in API <= 18, so we need to compute it + // The values form a unit quaternion, so we can compute the angle of + // rotation purely based on the given 3 values. + w = + 1.0f + - s.values[0] * s.values[0] + - s.values[1] * s.values[1] + - s.values[2] * s.values[2]; + w = (w > 0.0f) ? (float) Math.sqrt(w) : 0.0f; + } + break; + } + + GeckoAppShell.onSensorChanged(halType, x, y, z, w, time); + } + + // Geolocation. + @Override + public void onLocationChanged(final Location location) { + // No logging here: user-identifying information. + + final double altitude = location.hasAltitude() ? location.getAltitude() : Double.NaN; + + final float accuracy = location.hasAccuracy() ? location.getAccuracy() : Float.NaN; + + final float altitudeAccuracy = + Build.VERSION.SDK_INT >= 26 && location.hasVerticalAccuracy() + ? location.getVerticalAccuracyMeters() + : Float.NaN; + + final float speed = location.hasSpeed() ? location.getSpeed() : Float.NaN; + + final float heading = location.hasBearing() ? location.getBearing() : Float.NaN; + + // nsGeoPositionCoords will convert NaNs to null for optional + // properties of the JavaScript Coordinates object. + GeckoAppShell.onLocationChanged( + location.getLatitude(), + location.getLongitude(), + altitude, + accuracy, + altitudeAccuracy, + heading, + speed); + } + + @Override + public void onProviderDisabled(final String provider) {} + + @Override + public void onProviderEnabled(final String provider) {} + + @Override + public void onStatusChanged(final String provider, final int status, final Bundle extras) {} + } + + private static final AndroidListeners sAndroidListeners = new AndroidListeners(); + + private static SimpleArrayMap<String, PowerManager.WakeLock> sWakeLocks; + + /** Wake-lock for the CPU. */ + static final String WAKE_LOCK_CPU = "cpu"; + + /** Wake-lock for the screen. */ + static final String WAKE_LOCK_SCREEN = "screen"; + + /** Wake-lock for the audio-playing, eqaul to LOCK_CPU. */ + static final String WAKE_LOCK_AUDIO_PLAYING = "audio-playing"; + + /** Wake-lock for the video-playing, eqaul to LOCK_SCREEN.. */ + static final String WAKE_LOCK_VIDEO_PLAYING = "video-playing"; + + static final int WAKE_LOCKS_COUNT = 2; + + /** No one holds the wake-lock. */ + static final int WAKE_LOCK_STATE_UNLOCKED = 0; + + /** The wake-lock is held by a foreground window. */ + static final int WAKE_LOCK_STATE_LOCKED_FOREGROUND = 1; + + /** The wake-lock is held by a background window. */ + static final int WAKE_LOCK_STATE_LOCKED_BACKGROUND = 2; + + @SuppressLint("Wakelock") // We keep the wake lock independent from the function + // scope, so we need to suppress the linter warning. + private static void setWakeLockState(final String lock, final int state) { + if (sWakeLocks == null) { + sWakeLocks = new SimpleArrayMap<>(WAKE_LOCKS_COUNT); + } + + PowerManager.WakeLock wl = sWakeLocks.get(lock); + + // we should still hold the lock for background audio. + if (WAKE_LOCK_AUDIO_PLAYING.equals(lock) && state == WAKE_LOCK_STATE_LOCKED_BACKGROUND) { + return; + } + + if (state == WAKE_LOCK_STATE_LOCKED_FOREGROUND && wl == null) { + final PowerManager pm = + (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE); + + if (WAKE_LOCK_CPU.equals(lock) || WAKE_LOCK_AUDIO_PLAYING.equals(lock)) { + wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, lock); + } else if (WAKE_LOCK_SCREEN.equals(lock) || WAKE_LOCK_VIDEO_PLAYING.equals(lock)) { + // ON_AFTER_RELEASE is set, the user activity timer will be reset when the + // WakeLock is released, causing the illumination to remain on a bit longer. + wl = + pm.newWakeLock( + PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, lock); + } else { + Log.w(LOGTAG, "Unsupported wake-lock: " + lock); + return; + } + + wl.acquire(); + sWakeLocks.put(lock, wl); + } else if (state != WAKE_LOCK_STATE_LOCKED_FOREGROUND && wl != null) { + wl.release(); + sWakeLocks.remove(lock); + } + } + + @SuppressWarnings("fallthrough") + @WrapForJNI(calledFrom = "gecko") + private static void enableSensor(final int aSensortype) { + final SensorManager sm = + (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE); + + switch (aSensortype) { + case SENSOR_GAME_ROTATION_VECTOR: + if (gGameRotationVectorSensor == null) { + gGameRotationVectorSensor = sm.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR); + } + if (gGameRotationVectorSensor != null) { + sm.registerListener( + sAndroidListeners, gGameRotationVectorSensor, SensorManager.SENSOR_DELAY_FASTEST); + } + if (gGameRotationVectorSensor != null) { + break; + } + // Fallthrough + + case SENSOR_ROTATION_VECTOR: + if (gRotationVectorSensor == null) { + gRotationVectorSensor = sm.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); + } + if (gRotationVectorSensor != null) { + sm.registerListener( + sAndroidListeners, gRotationVectorSensor, SensorManager.SENSOR_DELAY_FASTEST); + } + if (gRotationVectorSensor != null) { + break; + } + // Fallthrough + + case SENSOR_ORIENTATION: + if (gOrientationSensor == null) { + gOrientationSensor = sm.getDefaultSensor(Sensor.TYPE_ORIENTATION); + } + if (gOrientationSensor != null) { + sm.registerListener( + sAndroidListeners, gOrientationSensor, SensorManager.SENSOR_DELAY_FASTEST); + } + break; + + case SENSOR_ACCELERATION: + if (gAccelerometerSensor == null) { + gAccelerometerSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + if (gAccelerometerSensor != null) { + sm.registerListener( + sAndroidListeners, gAccelerometerSensor, SensorManager.SENSOR_DELAY_FASTEST); + } + break; + + case SENSOR_LIGHT: + if (gLightSensor == null) { + gLightSensor = sm.getDefaultSensor(Sensor.TYPE_LIGHT); + } + if (gLightSensor != null) { + sm.registerListener(sAndroidListeners, gLightSensor, SensorManager.SENSOR_DELAY_NORMAL); + } + break; + + case SENSOR_LINEAR_ACCELERATION: + if (gLinearAccelerometerSensor == null) { + gLinearAccelerometerSensor = sm.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION); + } + if (gLinearAccelerometerSensor != null) { + sm.registerListener( + sAndroidListeners, gLinearAccelerometerSensor, SensorManager.SENSOR_DELAY_FASTEST); + } + break; + + case SENSOR_GYROSCOPE: + if (gGyroscopeSensor == null) { + gGyroscopeSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + } + if (gGyroscopeSensor != null) { + sm.registerListener( + sAndroidListeners, gGyroscopeSensor, SensorManager.SENSOR_DELAY_FASTEST); + } + break; + + default: + Log.w(LOGTAG, "Error! Can't enable unknown SENSOR type " + aSensortype); + } + } + + @SuppressWarnings("fallthrough") + @WrapForJNI(calledFrom = "gecko") + private static void disableSensor(final int aSensortype) { + final SensorManager sm = + (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE); + + switch (aSensortype) { + case SENSOR_GAME_ROTATION_VECTOR: + if (gGameRotationVectorSensor != null) { + sm.unregisterListener(sAndroidListeners, gGameRotationVectorSensor); + break; + } + // Fallthrough + + case SENSOR_ROTATION_VECTOR: + if (gRotationVectorSensor != null) { + sm.unregisterListener(sAndroidListeners, gRotationVectorSensor); + break; + } + // Fallthrough + + case SENSOR_ORIENTATION: + if (gOrientationSensor != null) { + sm.unregisterListener(sAndroidListeners, gOrientationSensor); + } + break; + + case SENSOR_ACCELERATION: + if (gAccelerometerSensor != null) { + sm.unregisterListener(sAndroidListeners, gAccelerometerSensor); + } + break; + + case SENSOR_LIGHT: + if (gLightSensor != null) { + sm.unregisterListener(sAndroidListeners, gLightSensor); + } + break; + + case SENSOR_LINEAR_ACCELERATION: + if (gLinearAccelerometerSensor != null) { + sm.unregisterListener(sAndroidListeners, gLinearAccelerometerSensor); + } + break; + + case SENSOR_GYROSCOPE: + if (gGyroscopeSensor != null) { + sm.unregisterListener(sAndroidListeners, gGyroscopeSensor); + } + break; + default: + Log.w(LOGTAG, "Error! Can't disable unknown SENSOR type " + aSensortype); + } + } + + @WrapForJNI(calledFrom = "gecko") + private static void moveTaskToBack() { + // This is a vestige, to be removed as full-screen support for GeckoView is implemented. + } + + @WrapForJNI(calledFrom = "gecko") + private static boolean hasHWVP8Encoder() { + return HardwareCodecCapabilityUtils.hasHWVP8(true /* aIsEncoder */); + } + + @WrapForJNI(calledFrom = "gecko") + private static boolean hasHWVP8Decoder() { + return HardwareCodecCapabilityUtils.hasHWVP8(false /* aIsEncoder */); + } + + @WrapForJNI(calledFrom = "gecko") + public static String getExtensionFromMimeType(final String aMimeType) { + return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType); + } + + @WrapForJNI(calledFrom = "gecko") + public static String getMimeTypeFromExtensions(final String aFileExt) { + final StringTokenizer st = new StringTokenizer(aFileExt, ".,; "); + String type = null; + String subType = null; + while (st.hasMoreElements()) { + final String ext = st.nextToken(); + final String mt = getMimeTypeFromExtension(ext); + if (mt == null) continue; + final int slash = mt.indexOf('/'); + final String tmpType = mt.substring(0, slash); + if (!tmpType.equalsIgnoreCase(type)) type = type == null ? tmpType : "*"; + final String tmpSubType = mt.substring(slash + 1); + if (!tmpSubType.equalsIgnoreCase(subType)) subType = subType == null ? tmpSubType : "*"; + } + if (type == null) type = "*"; + if (subType == null) subType = "*"; + return type + "/" + subType; + } + + @WrapForJNI(dispatchTo = "gecko") + private static native void notifyAlertListener(String name, String topic, String cookie); + + /** + * Called by the NotificationListener to notify Gecko that a previously shown notification has + * been closed. + */ + public static void onNotificationClose(final String name, final String cookie) { + if (GeckoThread.isRunning()) { + notifyAlertListener(name, "alertfinished", cookie); + } + } + + /** + * Called by the NotificationListener to notify Gecko that a previously shown notification has + * been clicked on. + */ + public static void onNotificationClick(final String name, final String cookie) { + if (GeckoThread.isRunning()) { + notifyAlertListener(name, "alertclickcallback", cookie); + } else { + GeckoThread.queueNativeCallUntil( + GeckoThread.State.PROFILE_READY, + GeckoAppShell.class, + "notifyAlertListener", + name, + "alertclickcallback", + cookie); + } + } + + public static synchronized void setDisplayDpiOverride(@Nullable final Integer dpi) { + if (dpi == null) { + return; + } + if (sDensityDpi != 0) { + Log.e(LOGTAG, "Tried to override screen DPI after it's already been set"); + return; + } + sDensityDpi = dpi; + } + + @WrapForJNI(calledFrom = "gecko") + public static synchronized int getDpi() { + if (sDensityDpi == 0) { + sDensityDpi = getApplicationContext().getResources().getDisplayMetrics().densityDpi; + } + return sDensityDpi; + } + + public static synchronized void setDisplayDensityOverride(@Nullable final Float density) { + if (density == null) { + return; + } + if (sDensity != null) { + Log.e(LOGTAG, "Tried to override screen density after it's already been set"); + return; + } + sDensity = density; + } + + @WrapForJNI(calledFrom = "gecko") + private static synchronized float getDensity() { + if (sDensity == null) { + sDensity = Float.valueOf(getApplicationContext().getResources().getDisplayMetrics().density); + } + + return sDensity; + } + + private static int sTotalRam; + + private static int getTotalRam(final Context context) { + if (sTotalRam == 0) { + final ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + final ActivityManager am = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + am.getMemoryInfo(memInfo); // `getMemoryInfo()` returns a value in B. Convert to MB. + sTotalRam = (int) (memInfo.totalMem / (1024 * 1024)); + Log.d(LOGTAG, "System memory: " + sTotalRam + "MB."); + } + + return sTotalRam; + } + + private static boolean isHighMemoryDevice(final Context context) { + return getTotalRam(context) > HIGH_MEMORY_DEVICE_THRESHOLD_MB; + } + + public static synchronized void useMaxScreenDepth(final boolean enable) { + sUseMaxScreenDepth = enable; + } + + /** Returns the colour depth of the default screen. This will either be 32, 24 or 16. */ + @WrapForJNI(calledFrom = "gecko") + public static synchronized int getScreenDepth() { + if (sScreenDepth == 0) { + sScreenDepth = 16; + final Context applicationContext = getApplicationContext(); + final PixelFormat info = new PixelFormat(); + final WindowManager wm = + (WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE); + PixelFormat.getPixelFormatInfo(wm.getDefaultDisplay().getPixelFormat(), info); + if (info.bitsPerPixel >= 24 && isHighMemoryDevice(applicationContext)) { + sScreenDepth = sUseMaxScreenDepth ? info.bitsPerPixel : 24; + } + } + + return sScreenDepth; + } + + @WrapForJNI(calledFrom = "gecko") + public static synchronized float getScreenRefreshRate() { + if (sScreenRefreshRate != null) { + return sScreenRefreshRate; + } + + final WindowManager wm = + (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + final float refreshRate = wm.getDefaultDisplay().getRefreshRate(); + // Android 11+ supports multiple refresh rate. So we have to get refresh rate per call. + // https://source.android.com/docs/core/graphics/multiple-refresh-rate + if (Build.VERSION.SDK_INT < 30) { + // Until Android 10, refresh rate is fixed, so we can cache it. + sScreenRefreshRate = Float.valueOf(refreshRate); + } + return refreshRate; + } + + @WrapForJNI(calledFrom = "gecko") + private static void performHapticFeedback(final boolean aIsLongPress) { + // Don't perform haptic feedback if a vibration is currently playing, + // because the haptic feedback will nuke the vibration. + if (!sVibrationMaybePlaying || System.nanoTime() >= sVibrationEndTime) { + final int[] pattern; + if (aIsLongPress) { + pattern = new int[] {0, 1, 20, 21}; + } else { + pattern = new int[] {0, 10, 20, 30}; + } + vibrateOnHapticFeedbackEnabled(pattern); + sVibrationMaybePlaying = false; + sVibrationEndTime = 0; + } + } + + private static Vibrator vibrator() { + return (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE); + } + + // Helper method to convert integer array to long array. + private static long[] convertIntToLongArray(final int[] input) { + final long[] output = new long[input.length]; + for (int i = 0; i < input.length; i++) { + output[i] = input[i]; + } + return output; + } + + // Vibrate only if haptic feedback is enabled. + private static void vibrateOnHapticFeedbackEnabled(final int[] milliseconds) { + if (Settings.System.getInt( + getApplicationContext().getContentResolver(), + Settings.System.HAPTIC_FEEDBACK_ENABLED, + 0) + > 0) { + if (milliseconds.length == 1) { + vibrate(milliseconds[0]); + } else { + vibrate(convertIntToLongArray(milliseconds), -1); + } + } + } + + @SuppressLint("MissingPermission") + @WrapForJNI(calledFrom = "gecko") + private static void vibrate(final long milliseconds) { + sVibrationEndTime = System.nanoTime() + milliseconds * 1000000; + sVibrationMaybePlaying = true; + try { + vibrator().vibrate(milliseconds); + } catch (final SecurityException ignore) { + Log.w(LOGTAG, "No VIBRATE permission"); + } + } + + @SuppressLint("MissingPermission") + @WrapForJNI(calledFrom = "gecko") + private static void vibrate(final long[] pattern, final int repeat) { + // If pattern.length is odd, the last element in the pattern is a + // meaningless delay, so don't include it in vibrationDuration. + long vibrationDuration = 0; + final int iterLen = pattern.length & ~1; + for (int i = 0; i < iterLen; i++) { + vibrationDuration += pattern[i]; + } + + sVibrationEndTime = System.nanoTime() + vibrationDuration * 1000000; + sVibrationMaybePlaying = true; + try { + vibrator().vibrate(pattern, repeat); + } catch (final SecurityException ignore) { + Log.w(LOGTAG, "No VIBRATE permission"); + } + } + + @SuppressLint("MissingPermission") + @WrapForJNI(calledFrom = "gecko") + private static void cancelVibrate() { + sVibrationMaybePlaying = false; + sVibrationEndTime = 0; + try { + vibrator().cancel(); + } catch (final SecurityException ignore) { + Log.w(LOGTAG, "No VIBRATE permission"); + } + } + + private static ConnectivityManager sConnectivityManager; + + private static void ensureConnectivityManager() { + if (sConnectivityManager == null) { + sConnectivityManager = + (ConnectivityManager) + getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); + } + } + + @WrapForJNI(calledFrom = "gecko") + private static boolean isNetworkLinkUp() { + ensureConnectivityManager(); + try { + final NetworkInfo info = sConnectivityManager.getActiveNetworkInfo(); + if (info == null || !info.isConnected()) return false; + } catch (final SecurityException se) { + return false; + } + return true; + } + + @WrapForJNI(calledFrom = "gecko") + private static boolean isNetworkLinkKnown() { + ensureConnectivityManager(); + try { + if (sConnectivityManager.getActiveNetworkInfo() == null) return false; + } catch (final SecurityException se) { + return false; + } + return true; + } + + @WrapForJNI(calledFrom = "gecko") + private static int getNetworkLinkType() { + ensureConnectivityManager(); + final NetworkInfo info = sConnectivityManager.getActiveNetworkInfo(); + if (info == null) { + return LINK_TYPE_UNKNOWN; + } + + switch (info.getType()) { + case ConnectivityManager.TYPE_ETHERNET: + return LINK_TYPE_ETHERNET; + case ConnectivityManager.TYPE_WIFI: + return LINK_TYPE_WIFI; + case ConnectivityManager.TYPE_WIMAX: + return LINK_TYPE_WIMAX; + case ConnectivityManager.TYPE_MOBILE: + return LINK_TYPE_MOBILE; + default: + Log.w(LOGTAG, "Ignoring the current network type."); + return LINK_TYPE_UNKNOWN; + } + } + + @WrapForJNI(calledFrom = "gecko", exceptionMode = "nsresult") + private static String getDNSDomains() { + if (Build.VERSION.SDK_INT < 23) { + return ""; + } + + ensureConnectivityManager(); + final Network net = sConnectivityManager.getActiveNetwork(); + if (net == null) { + return ""; + } + + final LinkProperties lp = sConnectivityManager.getLinkProperties(net); + if (lp == null) { + return ""; + } + + return lp.getDomains(); + } + + @SuppressLint("ResourceType") + @WrapForJNI(calledFrom = "gecko") + private static int[] getSystemColors() { + // attrsAppearance[] must correspond to AndroidSystemColors structure in android/nsLookAndFeel.h + final int[] attrsAppearance = { + android.R.attr.textColorPrimary, + android.R.attr.textColorPrimaryInverse, + android.R.attr.textColorSecondary, + android.R.attr.textColorSecondaryInverse, + android.R.attr.textColorTertiary, + android.R.attr.textColorTertiaryInverse, + android.R.attr.textColorHighlight, + android.R.attr.colorForeground, + android.R.attr.colorBackground, + android.R.attr.panelColorForeground, + android.R.attr.panelColorBackground, + Build.VERSION.SDK_INT >= 21 ? android.R.attr.colorAccent : 0, + }; + + final int[] result = new int[attrsAppearance.length]; + + final ContextThemeWrapper contextThemeWrapper = + new ContextThemeWrapper(getApplicationContext(), android.R.style.TextAppearance); + + final TypedArray appearance = contextThemeWrapper.obtainStyledAttributes(attrsAppearance); + + if (appearance != null) { + for (int i = 0; i < appearance.getIndexCount(); i++) { + final int idx = appearance.getIndex(i); + final int color = appearance.getColor(idx, 0); + result[idx] = color; + } + appearance.recycle(); + } + + return result; + } + + @WrapForJNI(calledFrom = "gecko") + private static byte[] getIconForExtension(final String aExt, final int iconSize) { + try { + int resolvedIconSize = iconSize; + if (iconSize <= 0) { + resolvedIconSize = 16; + } + + String resolvedExt = aExt; + if (aExt != null && aExt.length() > 1 && aExt.charAt(0) == '.') { + resolvedExt = aExt.substring(1); + } + + final PackageManager pm = getApplicationContext().getPackageManager(); + Drawable icon = getDrawableForExtension(pm, resolvedExt); + if (icon == null) { + // Use a generic icon. + icon = + ResourcesCompat.getDrawable( + getApplicationContext().getResources(), + R.drawable.ic_generic_file, + getApplicationContext().getTheme()); + } + + Bitmap bitmap = getBitmapFromDrawable(icon); + if (bitmap.getWidth() != resolvedIconSize || bitmap.getHeight() != resolvedIconSize) { + bitmap = Bitmap.createScaledBitmap(bitmap, resolvedIconSize, resolvedIconSize, true); + } + + final ByteBuffer buf = ByteBuffer.allocate(resolvedIconSize * resolvedIconSize * 4); + bitmap.copyPixelsToBuffer(buf); + + return buf.array(); + } catch (final Exception e) { + Log.w(LOGTAG, "getIconForExtension failed.", e); + return null; + } + } + + private static Bitmap getBitmapFromDrawable(final Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + int width = drawable.getIntrinsicWidth(); + width = width > 0 ? width : 1; + int height = drawable.getIntrinsicHeight(); + height = height > 0 ? height : 1; + + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return bitmap; + } + + public static String getMimeTypeFromExtension(final String ext) { + final MimeTypeMap mtm = MimeTypeMap.getSingleton(); + return mtm.getMimeTypeFromExtension(ext); + } + + private static Drawable getDrawableForExtension(final PackageManager pm, final String aExt) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + final String mimeType = getMimeTypeFromExtension(aExt); + if (mimeType != null && mimeType.length() > 0) intent.setType(mimeType); + else return null; + + final List<ResolveInfo> list = pm.queryIntentActivities(intent, 0); + if (list.size() == 0) return null; + + final ResolveInfo resolveInfo = list.get(0); + + if (resolveInfo == null) return null; + + final ActivityInfo activityInfo = resolveInfo.activityInfo; + + return activityInfo.loadIcon(pm); + } + + @WrapForJNI(calledFrom = "gecko") + private static boolean getShowPasswordSetting() { + try { + final int showPassword = + Settings.System.getInt( + getApplicationContext().getContentResolver(), Settings.System.TEXT_SHOW_PASSWORD, 1); + return (showPassword > 0); + } catch (final Exception e) { + return true; + } + } + + private static Context sApplicationContext; + private static Boolean sIs24HourFormat = true; + + @WrapForJNI + public static Context getApplicationContext() { + return sApplicationContext; + } + + public static void setApplicationContext(final Context context) { + sApplicationContext = context; + } + + /* + * Battery API related methods. + */ + @WrapForJNI(calledFrom = "gecko") + private static void enableBatteryNotifications() { + GeckoBatteryManager.enableNotifications(); + } + + @WrapForJNI(calledFrom = "gecko") + private static void disableBatteryNotifications() { + GeckoBatteryManager.disableNotifications(); + } + + @WrapForJNI(calledFrom = "gecko") + private static double[] getCurrentBatteryInformation() { + return GeckoBatteryManager.getCurrentInformation(); + } + + /* Called by JNI from AndroidBridge, and by reflection from tests/BaseTest.java.in */ + @WrapForJNI(calledFrom = "gecko") + @RobocopTarget + public static boolean isTablet() { + return HardwareUtils.isTablet(getApplicationContext()); + } + + @WrapForJNI(calledFrom = "gecko") + private static double[] getCurrentNetworkInformation() { + return GeckoNetworkManager.getInstance().getCurrentInformation(); + } + + @WrapForJNI(calledFrom = "gecko") + private static void enableNetworkNotifications() { + ThreadUtils.runOnUiThread(() -> GeckoNetworkManager.getInstance().enableNotifications()); + } + + @WrapForJNI(calledFrom = "gecko") + private static void disableNetworkNotifications() { + ThreadUtils.runOnUiThread( + new Runnable() { + @Override + public void run() { + GeckoNetworkManager.getInstance().disableNotifications(); + } + }); + } + + @WrapForJNI(calledFrom = "gecko") + private static short getScreenOrientation() { + return GeckoScreenOrientation.getInstance().getScreenOrientation().value; + } + + /* package */ static int getRotation() { + return sScreenCompat.getRotation(); + } + + @WrapForJNI(calledFrom = "gecko") + private static int getScreenAngle() { + return GeckoScreenOrientation.getInstance().getAngle(); + } + + @WrapForJNI(calledFrom = "gecko") + private static void notifyWakeLockChanged(final String topic, final String state) { + final int intState; + if ("unlocked".equals(state)) { + intState = WAKE_LOCK_STATE_UNLOCKED; + } else if ("locked-foreground".equals(state)) { + intState = WAKE_LOCK_STATE_LOCKED_FOREGROUND; + } else if ("locked-background".equals(state)) { + intState = WAKE_LOCK_STATE_LOCKED_BACKGROUND; + } else { + throw new IllegalArgumentException(); + } + setWakeLockState(topic, intState); + } + + @WrapForJNI(calledFrom = "gecko") + private static String getProxyForURI( + final String spec, final String scheme, final String host, final int port) { + final ProxySelector ps = new ProxySelector(); + + final Proxy proxy = ps.select(scheme, host); + if (Proxy.NO_PROXY.equals(proxy)) { + return "DIRECT"; + } + + switch (proxy.type()) { + case HTTP: + return "PROXY " + proxy.address().toString(); + case SOCKS: + return "SOCKS " + proxy.address().toString(); + } + + return "DIRECT"; + } + + @WrapForJNI(calledFrom = "gecko") + private static int getMaxTouchPoints() { + final PackageManager pm = getApplicationContext().getPackageManager(); + if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_JAZZHAND)) { + // at least, 5+ fingers. + return 5; + } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { + // at least, 2+ fingers. + return 2; + } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)) { + // 2 fingers + return 2; + } else if (pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) { + // 1 finger + return 1; + } + return 0; + } + + /* + * Keep in sync with PointerCapabilities in ServoTypes.h + */ + private static final int NO_POINTER = 0x00000000; + private static final int COARSE_POINTER = 0x00000001; + private static final int FINE_POINTER = 0x00000002; + private static final int HOVER_CAPABLE_POINTER = 0x00000004; + + private static int getPointerCapabilities(final InputDevice inputDevice) { + int result = NO_POINTER; + final int sources = inputDevice.getSources(); + + // Blink checks fine pointer at first, then it check coarse pointer. + // So, we should use same order for compatibility. + // Also, if using Chrome OS, source may be SOURCE_MOUSE | SOURCE_TOUCHSCREEN | SOURCE_STYLUS + // even if no touch screen. So we shouldn't check TOUCHSCREEN at first. + + if (hasInputDeviceSource(sources, InputDevice.SOURCE_MOUSE) + || hasInputDeviceSource(sources, InputDevice.SOURCE_STYLUS) + || hasInputDeviceSource(sources, InputDevice.SOURCE_TOUCHPAD) + || hasInputDeviceSource(sources, InputDevice.SOURCE_TRACKBALL)) { + result |= FINE_POINTER; + } else if (hasInputDeviceSource(sources, InputDevice.SOURCE_TOUCHSCREEN) + || hasInputDeviceSource(sources, InputDevice.SOURCE_JOYSTICK)) { + result |= COARSE_POINTER; + } + + if (hasInputDeviceSource(sources, InputDevice.SOURCE_MOUSE) + || hasInputDeviceSource(sources, InputDevice.SOURCE_TOUCHPAD) + || hasInputDeviceSource(sources, InputDevice.SOURCE_TRACKBALL) + || hasInputDeviceSource(sources, InputDevice.SOURCE_JOYSTICK)) { + result |= HOVER_CAPABLE_POINTER; + } + + return result; + } + + @WrapForJNI(calledFrom = "gecko") + // For any-pointer and any-hover media queries features. + private static int getAllPointerCapabilities() { + int result = NO_POINTER; + + for (final int deviceId : InputDevice.getDeviceIds()) { + final InputDevice inputDevice = InputDevice.getDevice(deviceId); + if (inputDevice == null || !InputDeviceUtils.isPointerTypeDevice(inputDevice)) { + continue; + } + + result |= getPointerCapabilities(inputDevice); + } + + return result; + } + + private static boolean hasInputDeviceSource(final int sources, final int inputDeviceSource) { + return (sources & inputDeviceSource) == inputDeviceSource; + } + + public static synchronized void setScreenSizeOverride(final Rect size) { + sScreenSizeOverride = size; + } + + static final ScreenCompat sScreenCompat; + + private interface ScreenCompat { + Rect getScreenSize(); + + int getRotation(); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private static class JellyBeanScreenCompat implements ScreenCompat { + public Rect getScreenSize() { + final WindowManager wm = + (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + final Display disp = wm.getDefaultDisplay(); + return new Rect(0, 0, disp.getWidth(), disp.getHeight()); + } + + public int getRotation() { + final WindowManager wm = + (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + return wm.getDefaultDisplay().getRotation(); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private static class JellyBeanMR1ScreenCompat implements ScreenCompat { + public Rect getScreenSize() { + final WindowManager wm = + (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + final Display disp = wm.getDefaultDisplay(); + final Point size = new Point(); + disp.getRealSize(size); + return new Rect(0, 0, size.x, size.y); + } + + public int getRotation() { + final WindowManager wm = + (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); + return wm.getDefaultDisplay().getRotation(); + } + } + + @TargetApi(Build.VERSION_CODES.S) + private static class AndroidSScreenCompat implements ScreenCompat { + @SuppressLint("StaticFieldLeak") + private static Context sWindowContext; + + private static Context getWindowContext() { + if (sWindowContext == null) { + final DisplayManager displayManager = + (DisplayManager) getApplicationContext().getSystemService(Context.DISPLAY_SERVICE); + final Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + sWindowContext = + getApplicationContext() + .createWindowContext(display, WindowManager.LayoutParams.TYPE_APPLICATION, null); + } + return sWindowContext; + } + + public Rect getScreenSize() { + final WindowManager windowManager = getWindowContext().getSystemService(WindowManager.class); + return windowManager.getCurrentWindowMetrics().getBounds(); + } + + public int getRotation() { + final WindowManager windowManager = getWindowContext().getSystemService(WindowManager.class); + return windowManager.getDefaultDisplay().getRotation(); + } + } + + static { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + sScreenCompat = new AndroidSScreenCompat(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + sScreenCompat = new JellyBeanMR1ScreenCompat(); + } else { + sScreenCompat = new JellyBeanScreenCompat(); + } + } + + /* package */ static Rect getScreenSizeIgnoreOverride() { + return sScreenCompat.getScreenSize(); + } + + @WrapForJNI(calledFrom = "gecko") + private static synchronized Rect getScreenSize() { + if (sScreenSizeOverride != null) { + return sScreenSizeOverride; + } + + return getScreenSizeIgnoreOverride(); + } + + @WrapForJNI(calledFrom = "any") + public static int getAudioOutputFramesPerBuffer() { + final int DEFAULT = 512; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return DEFAULT; + } + final AudioManager am = + (AudioManager) getApplicationContext().getSystemService(Context.AUDIO_SERVICE); + if (am == null) { + return DEFAULT; + } + final String prop = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER); + if (prop == null) { + return DEFAULT; + } + return Integer.parseInt(prop); + } + + @WrapForJNI(calledFrom = "any") + public static int getAudioOutputSampleRate() { + final int DEFAULT = 44100; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return DEFAULT; + } + final AudioManager am = + (AudioManager) getApplicationContext().getSystemService(Context.AUDIO_SERVICE); + if (am == null) { + return DEFAULT; + } + final String prop = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); + if (prop == null) { + return DEFAULT; + } + return Integer.parseInt(prop); + } + + @WrapForJNI(calledFrom = "any") + public static void setCommunicationAudioModeOn(final boolean on) { + final AudioManager am = + (AudioManager) getApplicationContext().getSystemService(Context.AUDIO_SERVICE); + if (am == null) { + return; + } + + try { + if (on) { + Log.e(LOGTAG, "Setting communication mode ON"); + // This shouldn't throw, but does throw NullPointerException on a very + // small number of devices. + am.startBluetoothSco(); + am.setBluetoothScoOn(true); + } else { + Log.e(LOGTAG, "Setting communication mode OFF"); + am.stopBluetoothSco(); + am.setBluetoothScoOn(false); + } + } catch (final SecurityException | NullPointerException e) { + Log.e(LOGTAG, "could not set communication mode", e); + } + } + + private static String getLanguageTag(final Locale locale) { + final StringBuilder out = new StringBuilder(locale.getLanguage()); + final String country = locale.getCountry(); + final String variant = locale.getVariant(); + if (!TextUtils.isEmpty(country)) { + out.append('-').append(country); + } + if (!TextUtils.isEmpty(variant)) { + out.append('-').append(variant); + } + // e.g. "en", "en-US", or "en-US-POSIX". + return out.toString(); + } + + @WrapForJNI + public static String[] getDefaultLocales() { + // XXX We may have to convert some language codes such as "id" vs "in". + if (Build.VERSION.SDK_INT >= 24) { + final LocaleList localeList = LocaleList.getDefault(); + final String[] locales = new String[localeList.size()]; + for (int i = 0; i < localeList.size(); i++) { + locales[i] = localeList.get(i).toLanguageTag(); + } + return locales; + } + final String[] locales = new String[1]; + final Locale locale = Locale.getDefault(); + if (Build.VERSION.SDK_INT >= 21) { + locales[0] = locale.toLanguageTag(); + return locales; + } + + locales[0] = getLanguageTag(locale); + return locales; + } + + public static void setIs24HourFormat(final Boolean is24HourFormat) { + sIs24HourFormat = is24HourFormat; + } + + @WrapForJNI + public static boolean getIs24HourFormat() { + return sIs24HourFormat; + } + + @WrapForJNI + public static String getAppName() { + final Context context = getApplicationContext(); + final ApplicationInfo info = context.getApplicationInfo(); + final int id = info.labelRes; + return id == 0 ? info.nonLocalizedLabel.toString() : context.getString(id); + } + + @WrapForJNI(calledFrom = "gecko") + private static int getMemoryUsage(final String stateName) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // No API to get Java heap usages. + return -1; + } + + final Debug.MemoryInfo memInfo = new Debug.MemoryInfo(); + Debug.getMemoryInfo(memInfo); + final String usage = memInfo.getMemoryStat(stateName); + if (usage == null) { + return -1; + } + try { + return Integer.parseInt(usage); + } catch (final NumberFormatException e) { + return -1; + } + } + + @WrapForJNI + public static native boolean isParentProcess(); + + /** + * Returns a GeckoResult that will be completed to true if the GPU process is enabled and false if + * it is disabled. + */ + @WrapForJNI + public static native GeckoResult<Boolean> isGpuProcessEnabled(); + + @SuppressLint("NewApi") + public static boolean isIsolatedProcess() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { + return false; + } + // This method was added in SDK 16 but remained hidden until SDK 28, meaning we are okay to call + // this on any SDK level but must suppress the new API lint. + return android.os.Process.isIsolated(); + } +} |