/* -*- 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.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.InetSocketAddress; 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.CrashHandler; 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 handlerService) { super(handlerService); } @Override public String getAppPackageName() { final Context appContext = getAppContext(); if (appContext == null) { return ""; } return appContext.getPackageName(); } @Override public Context getAppContext() { return getApplicationContext(); } @SuppressLint("ApplySharedPref") @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 handler) { if (sCrashHandler == null) { sCrashHandler = new GeckoCrashHandler(handler); } return sCrashHandler; } private static Class sCrashHandlerService; public static synchronized void setCrashHandlerService( final Class handlerService) { sCrashHandlerService = handlerService; } public static synchronized Class getCrashHandlerService() { return sCrashHandlerService; } @WrapForJNI(exceptionMode = "ignore") public 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 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 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, android.R.attr.colorAccent, }; 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 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(getApplicationContext()); } /* 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"; } final InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); final String proxyString = proxyAddress.getHostString() + ":" + proxyAddress.getPort(); switch (proxy.type()) { case HTTP: return "PROXY " + proxyString; case SOCKS: return "SOCKS " + proxyString; } 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(); } 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 { sScreenCompat = new JellyBeanMR1ScreenCompat(); } } /* 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; 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; 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); } } @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(); locales[0] = locale.toLanguageTag(); 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 isGpuProcessEnabled(); @SuppressLint("NewApi") public static boolean isIsolatedProcess() { // 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(); } }