diff options
Diffstat (limited to 'base/android/java')
60 files changed, 10738 insertions, 0 deletions
diff --git a/base/android/java/src/org/chromium/base/ActivityState.java b/base/android/java/src/org/chromium/base/ActivityState.java new file mode 100644 index 0000000000..b14814c1c0 --- /dev/null +++ b/base/android/java/src/org/chromium/base/ActivityState.java @@ -0,0 +1,48 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A set of states that represent the last state change of an Activity. + */ +@Retention(RetentionPolicy.SOURCE) +@IntDef({ActivityState.CREATED, ActivityState.STARTED, ActivityState.RESUMED, ActivityState.PAUSED, + ActivityState.STOPPED, ActivityState.DESTROYED}) +public @interface ActivityState { + /** + * Represents Activity#onCreate(). + */ + int CREATED = 1; + + /** + * Represents Activity#onStart(). + */ + int STARTED = 2; + + /** + * Represents Activity#onResume(). + */ + int RESUMED = 3; + + /** + * Represents Activity#onPause(). + */ + int PAUSED = 4; + + /** + * Represents Activity#onStop(). + */ + int STOPPED = 5; + + /** + * Represents Activity#onDestroy(). This is also used when the state of an Activity is unknown. + */ + int DESTROYED = 6; +} diff --git a/base/android/java/src/org/chromium/base/AnimationFrameTimeHistogram.java b/base/android/java/src/org/chromium/base/AnimationFrameTimeHistogram.java new file mode 100644 index 0000000000..ad5cdd815b --- /dev/null +++ b/base/android/java/src/org/chromium/base/AnimationFrameTimeHistogram.java @@ -0,0 +1,145 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeAnimator; +import android.animation.TimeAnimator.TimeListener; +import android.util.Log; + +/** + * Record Android animation frame rate and save it to UMA histogram. This is mainly for monitoring + * any jankiness of short Chrome Android animations. It is limited to few seconds of recording. + */ +public class AnimationFrameTimeHistogram { + private static final String TAG = "AnimationFrameTimeHistogram"; + private static final int MAX_FRAME_TIME_NUM = 600; // 10 sec on 60 fps. + + private final Recorder mRecorder = new Recorder(); + private final String mHistogramName; + + /** + * @param histogramName The histogram name that the recorded frame times will be saved. + * This must be also defined in histograms.xml + * @return An AnimatorListener instance that records frame time histogram on start and end + * automatically. + */ + public static AnimatorListener getAnimatorRecorder(final String histogramName) { + return new AnimatorListenerAdapter() { + private final AnimationFrameTimeHistogram mAnimationFrameTimeHistogram = + new AnimationFrameTimeHistogram(histogramName); + + @Override + public void onAnimationStart(Animator animation) { + mAnimationFrameTimeHistogram.startRecording(); + } + + @Override + public void onAnimationEnd(Animator animation) { + mAnimationFrameTimeHistogram.endRecording(); + } + + @Override + public void onAnimationCancel(Animator animation) { + mAnimationFrameTimeHistogram.endRecording(); + } + }; + } + + /** + * @param histogramName The histogram name that the recorded frame times will be saved. + * This must be also defined in histograms.xml + */ + public AnimationFrameTimeHistogram(String histogramName) { + mHistogramName = histogramName; + } + + /** + * Start recording frame times. The recording can fail if it exceeds a few seconds. + */ + public void startRecording() { + mRecorder.startRecording(); + } + + /** + * End recording and save it to histogram. It won't save histogram if the recording wasn't + * successful. + */ + public void endRecording() { + if (mRecorder.endRecording()) { + nativeSaveHistogram(mHistogramName, + mRecorder.getFrameTimesMs(), mRecorder.getFrameTimesCount()); + } + mRecorder.cleanUp(); + } + + /** + * Record Android animation frame rate and return the result. + */ + private static class Recorder implements TimeListener { + // TODO(kkimlabs): If we can use in the future, migrate to Choreographer for minimal + // workload. + private final TimeAnimator mAnimator = new TimeAnimator(); + private long[] mFrameTimesMs; + private int mFrameTimesCount; + + private Recorder() { + mAnimator.setTimeListener(this); + } + + private void startRecording() { + assert !mAnimator.isRunning(); + mFrameTimesCount = 0; + mFrameTimesMs = new long[MAX_FRAME_TIME_NUM]; + mAnimator.start(); + } + + /** + * @return Whether the recording was successful. If successful, the result is available via + * getFrameTimesNs and getFrameTimesCount. + */ + private boolean endRecording() { + boolean succeeded = mAnimator.isStarted(); + mAnimator.end(); + return succeeded; + } + + private long[] getFrameTimesMs() { + return mFrameTimesMs; + } + + private int getFrameTimesCount() { + return mFrameTimesCount; + } + + /** + * Deallocates the temporary buffer to record frame times. Must be called after ending + * the recording and getting the result. + */ + private void cleanUp() { + mFrameTimesMs = null; + } + + @Override + public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { + if (mFrameTimesCount == mFrameTimesMs.length) { + mAnimator.end(); + cleanUp(); + Log.w(TAG, "Animation frame time recording reached the maximum number. It's either" + + "the animation took too long or recording end is not called."); + return; + } + + // deltaTime is 0 for the first frame. + if (deltaTime > 0) { + mFrameTimesMs[mFrameTimesCount++] = deltaTime; + } + } + } + + private native void nativeSaveHistogram(String histogramName, long[] frameTimesMs, int count); +} diff --git a/base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java b/base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java new file mode 100644 index 0000000000..d1c4693c4a --- /dev/null +++ b/base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java @@ -0,0 +1,705 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.VectorDrawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.os.Process; +import android.os.StatFs; +import android.os.StrictMode; +import android.os.UserManager; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.text.Html; +import android.text.Spanned; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodSubtype; +import android.view.textclassifier.TextClassifier; +import android.widget.TextView; + +import java.io.File; +import java.io.UnsupportedEncodingException; + +/** + * Utility class to use new APIs that were added after ICS (API level 14). + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class ApiCompatibilityUtils { + private ApiCompatibilityUtils() { + } + + /** + * Compares two long values numerically. The value returned is identical to what would be + * returned by {@link Long#compare(long, long)} which is available since API level 19. + */ + public static int compareLong(long lhs, long rhs) { + return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); + } + + /** + * Compares two boolean values. The value returned is identical to what would be returned by + * {@link Boolean#compare(boolean, boolean)} which is available since API level 19. + */ + public static int compareBoolean(boolean lhs, boolean rhs) { + return lhs == rhs ? 0 : lhs ? 1 : -1; + } + + /** + * Checks that the object reference is not null and throws NullPointerException if it is. + * See {@link Objects#requireNonNull} which is available since API level 19. + * @param obj The object to check + */ + @NonNull + public static <T> T requireNonNull(T obj) { + if (obj == null) throw new NullPointerException(); + return obj; + } + + /** + * Checks that the object reference is not null and throws NullPointerException if it is. + * See {@link Objects#requireNonNull} which is available since API level 19. + * @param obj The object to check + * @param message The message to put into NullPointerException + */ + @NonNull + public static <T> T requireNonNull(T obj, String message) { + if (obj == null) throw new NullPointerException(message); + return obj; + } + + /** + * {@link String#getBytes()} but specifying UTF-8 as the encoding and capturing the resulting + * UnsupportedEncodingException. + */ + public static byte[] getBytesUtf8(String str) { + try { + return str.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8 encoding not available.", e); + } + } + + /** + * Returns true if view's layout direction is right-to-left. + * + * @param view the View whose layout is being considered + */ + public static boolean isLayoutRtl(View view) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } else { + // All layouts are LTR before JB MR1. + return false; + } + } + + /** + * @see Configuration#getLayoutDirection() + */ + public static int getLayoutDirection(Configuration configuration) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return configuration.getLayoutDirection(); + } else { + // All layouts are LTR before JB MR1. + return View.LAYOUT_DIRECTION_LTR; + } + } + + /** + * @return True if the running version of the Android supports printing. + */ + public static boolean isPrintingSupported() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + } + + /** + * @return True if the running version of the Android supports elevation. Elevation of a view + * determines the visual appearance of its shadow. + */ + public static boolean isElevationSupported() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + } + + /** + * @see android.view.View#setLayoutDirection(int) + */ + public static void setLayoutDirection(View view, int layoutDirection) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + view.setLayoutDirection(layoutDirection); + } else { + // Do nothing. RTL layouts aren't supported before JB MR1. + } + } + + /** + * @see android.view.View#setTextAlignment(int) + */ + public static void setTextAlignment(View view, int textAlignment) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + view.setTextAlignment(textAlignment); + } else { + // Do nothing. RTL text isn't supported before JB MR1. + } + } + + /** + * @see android.view.View#setTextDirection(int) + */ + public static void setTextDirection(View view, int textDirection) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + view.setTextDirection(textDirection); + } else { + // Do nothing. RTL text isn't supported before JB MR1. + } + } + + /** + * See {@link android.view.View#setLabelFor(int)}. + */ + public static void setLabelFor(View labelView, int id) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + labelView.setLabelFor(id); + } else { + // Do nothing. #setLabelFor() isn't supported before JB MR1. + } + } + + /** + * @see android.widget.TextView#getCompoundDrawablesRelative() + */ + public static Drawable[] getCompoundDrawablesRelative(TextView textView) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return textView.getCompoundDrawablesRelative(); + } else { + return textView.getCompoundDrawables(); + } + } + + /** + * @see android.widget.TextView#setCompoundDrawablesRelative(Drawable, Drawable, Drawable, + * Drawable) + */ + public static void setCompoundDrawablesRelative(TextView textView, Drawable start, Drawable top, + Drawable end, Drawable bottom) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { + // On JB MR1, due to a platform bug, setCompoundDrawablesRelative() is a no-op if the + // view has ever been measured. As a workaround, use setCompoundDrawables() directly. + // See: http://crbug.com/368196 and http://crbug.com/361709 + boolean isRtl = isLayoutRtl(textView); + textView.setCompoundDrawables(isRtl ? end : start, top, isRtl ? start : end, bottom); + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) { + textView.setCompoundDrawablesRelative(start, top, end, bottom); + } else { + textView.setCompoundDrawables(start, top, end, bottom); + } + } + + /** + * @see android.widget.TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable, + * Drawable, Drawable, Drawable) + */ + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(TextView textView, + Drawable start, Drawable top, Drawable end, Drawable bottom) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { + // Work around the platform bug described in setCompoundDrawablesRelative() above. + boolean isRtl = isLayoutRtl(textView); + textView.setCompoundDrawablesWithIntrinsicBounds(isRtl ? end : start, top, + isRtl ? start : end, bottom); + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) { + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); + } else { + textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + } + } + + /** + * @see android.widget.TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(int, int, int, + * int) + */ + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(TextView textView, + int start, int top, int end, int bottom) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { + // Work around the platform bug described in setCompoundDrawablesRelative() above. + boolean isRtl = isLayoutRtl(textView); + textView.setCompoundDrawablesWithIntrinsicBounds(isRtl ? end : start, top, + isRtl ? start : end, bottom); + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) { + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); + } else { + textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + } + } + + /** + * @see android.text.Html#toHtml(Spanned, int) + * @param option is ignored on below N + */ + @SuppressWarnings("deprecation") + public static String toHtml(Spanned spanned, int option) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return Html.toHtml(spanned, option); + } else { + return Html.toHtml(spanned); + } + } + + // These methods have a new name, and the old name is deprecated. + + /** + * @see android.app.PendingIntent#getCreatorPackage() + */ + @SuppressWarnings("deprecation") + public static String getCreatorPackage(PendingIntent intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return intent.getCreatorPackage(); + } else { + return intent.getTargetPackage(); + } + } + + /** + * @see android.provider.Settings.Global#DEVICE_PROVISIONED + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isDeviceProvisioned(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) return true; + if (context == null) return true; + if (context.getContentResolver() == null) return true; + return Settings.Global.getInt( + context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0; + } + + /** + * @see android.app.Activity#finishAndRemoveTask() + */ + public static void finishAndRemoveTask(Activity activity) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { + activity.finishAndRemoveTask(); + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { + // crbug.com/395772 : Fallback for Activity.finishAndRemoveTask() failing. + new FinishAndRemoveTaskWithRetry(activity).run(); + } else { + activity.finish(); + } + } + + /** + * Set elevation if supported. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public static boolean setElevation(View view, float elevationValue) { + if (!isElevationSupported()) return false; + + view.setElevation(elevationValue); + return true; + } + + /** + * Gets an intent to start the Android system notification settings activity for an app. + * + * @param context Context of the app whose settings intent should be returned. + */ + public static Intent getNotificationSettingsIntent(Context context) { + Intent intent = new Intent(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); + } else { + intent.setAction("android.settings.ACTION_APP_NOTIFICATION_SETTINGS"); + intent.putExtra("app_package", context.getPackageName()); + intent.putExtra("app_uid", context.getApplicationInfo().uid); + } + return intent; + } + + private static class FinishAndRemoveTaskWithRetry implements Runnable { + private static final long RETRY_DELAY_MS = 500; + private static final long MAX_TRY_COUNT = 3; + private final Activity mActivity; + private int mTryCount; + + FinishAndRemoveTaskWithRetry(Activity activity) { + mActivity = activity; + } + + @Override + public void run() { + mActivity.finishAndRemoveTask(); + mTryCount++; + if (!mActivity.isFinishing()) { + if (mTryCount < MAX_TRY_COUNT) { + ThreadUtils.postOnUiThreadDelayed(this, RETRY_DELAY_MS); + } else { + mActivity.finish(); + } + } + } + } + + /** + * @return Whether the screen of the device is interactive. + */ + @SuppressWarnings("deprecation") + public static boolean isInteractive(Context context) { + PowerManager manager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + return manager.isInteractive(); + } else { + return manager.isScreenOn(); + } + } + + @SuppressWarnings("deprecation") + public static int getActivityNewDocumentFlag() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return Intent.FLAG_ACTIVITY_NEW_DOCUMENT; + } else { + return Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET; + } + } + + /** + * @see android.provider.Settings.Secure#SKIP_FIRST_USE_HINTS + */ + public static boolean shouldSkipFirstUseHints(ContentResolver contentResolver) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return Settings.Secure.getInt( + contentResolver, Settings.Secure.SKIP_FIRST_USE_HINTS, 0) != 0; + } else { + return false; + } + } + + /** + * @param activity Activity that should get the task description update. + * @param title Title of the activity. + * @param icon Icon of the activity. + * @param color Color of the activity. It must be a fully opaque color. + */ + public static void setTaskDescription(Activity activity, String title, Bitmap icon, int color) { + // TaskDescription requires an opaque color. + assert Color.alpha(color) == 255; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ActivityManager.TaskDescription description = + new ActivityManager.TaskDescription(title, icon, color); + activity.setTaskDescription(description); + } + } + + /** + * @see android.view.Window#setStatusBarColor(int color). + */ + public static void setStatusBarColor(Window window, int statusBarColor) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return; + + // If both system bars are black, we can remove these from our layout, + // removing or shrinking the SurfaceFlinger overlay required for our views. + // This benefits battery usage on L and M. However, this no longer provides a battery + // benefit as of N and starts to cause flicker bugs on O, so don't bother on O and up. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && statusBarColor == Color.BLACK + && window.getNavigationBarColor() == Color.BLACK) { + window.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + } + window.setStatusBarColor(statusBarColor); + } + + /** + * Sets the status bar icons to dark or light. Note that this is only valid for + * Android M+. + * + * @param rootView The root view used to request updates to the system UI theming. + * @param useDarkIcons Whether the status bar icons should be dark. + */ + public static void setStatusBarIconColor(View rootView, boolean useDarkIcons) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return; + + int systemUiVisibility = rootView.getSystemUiVisibility(); + if (useDarkIcons) { + systemUiVisibility |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } else { + systemUiVisibility &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } + rootView.setSystemUiVisibility(systemUiVisibility); + } + + /** + * @see android.content.res.Resources#getDrawable(int id). + * TODO(ltian): use {@link AppCompatResources} to parse drawable to prevent fail on + * {@link VectorDrawable}. (http://crbug.com/792129) + */ + @SuppressWarnings("deprecation") + public static Drawable getDrawable(Resources res, int id) throws NotFoundException { + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return res.getDrawable(id, null); + } else { + return res.getDrawable(id); + } + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + } + + /** + * @see android.content.res.Resources#getDrawableForDensity(int id, int density). + */ + @SuppressWarnings("deprecation") + public static Drawable getDrawableForDensity(Resources res, int id, int density) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return res.getDrawableForDensity(id, density, null); + } else { + return res.getDrawableForDensity(id, density); + } + } + + /** + * @see android.app.Activity#finishAfterTransition(). + */ + public static void finishAfterTransition(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.finishAfterTransition(); + } else { + activity.finish(); + } + } + + /** + * @see android.content.pm.PackageManager#getUserBadgedIcon(Drawable, android.os.UserHandle). + */ + public static Drawable getUserBadgedIcon(Context context, int id) { + Drawable drawable = getDrawable(context.getResources(), id); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + PackageManager packageManager = context.getPackageManager(); + drawable = packageManager.getUserBadgedIcon(drawable, Process.myUserHandle()); + } + return drawable; + } + + /** + * @see android.content.pm.PackageManager#getUserBadgedDrawableForDensity(Drawable drawable, + * UserHandle user, Rect badgeLocation, int badgeDensity). + */ + public static Drawable getUserBadgedDrawableForDensity( + Context context, Drawable drawable, Rect badgeLocation, int density) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + PackageManager packageManager = context.getPackageManager(); + return packageManager.getUserBadgedDrawableForDensity( + drawable, Process.myUserHandle(), badgeLocation, density); + } + return drawable; + } + + /** + * @see android.content.res.Resources#getColor(int id). + */ + @SuppressWarnings("deprecation") + public static int getColor(Resources res, int id) throws NotFoundException { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return res.getColor(id, null); + } else { + return res.getColor(id); + } + } + + /** + * @see android.graphics.drawable.Drawable#getColorFilter(). + */ + @SuppressWarnings("NewApi") + public static ColorFilter getColorFilter(Drawable drawable) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return drawable.getColorFilter(); + } else { + return null; + } + } + + /** + * @see android.widget.TextView#setTextAppearance(int id). + */ + @SuppressWarnings("deprecation") + public static void setTextAppearance(TextView view, int id) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + view.setTextAppearance(id); + } else { + view.setTextAppearance(view.getContext(), id); + } + } + + /** + * See {@link android.os.StatFs#getAvailableBlocksLong}. + */ + @SuppressWarnings("deprecation") + public static long getAvailableBlocks(StatFs statFs) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return statFs.getAvailableBlocksLong(); + } else { + return statFs.getAvailableBlocks(); + } + } + + /** + * See {@link android.os.StatFs#getBlockCount}. + */ + @SuppressWarnings("deprecation") + public static long getBlockCount(StatFs statFs) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return statFs.getBlockCountLong(); + } else { + return statFs.getBlockCount(); + } + } + + /** + * See {@link android.os.StatFs#getBlockSize}. + */ + @SuppressWarnings("deprecation") + public static long getBlockSize(StatFs statFs) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return statFs.getBlockSizeLong(); + } else { + return statFs.getBlockSize(); + } + } + + /** + * @param context The Android context, used to retrieve the UserManager system service. + * @return Whether the device is running in demo mode. + */ + @SuppressWarnings("NewApi") + public static boolean isDemoUser(Context context) { + // UserManager#isDemoUser() is only available in Android NMR1+. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return false; + + UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + return userManager.isDemoUser(); + } + + /** + * @see Context#checkPermission(String, int, int) + */ + public static int checkPermission(Context context, String permission, int pid, int uid) { + try { + return context.checkPermission(permission, pid, uid); + } catch (RuntimeException e) { + // Some older versions of Android throw odd errors when checking for permissions, so + // just swallow the exception and treat it as the permission is denied. + // crbug.com/639099 + return PackageManager.PERMISSION_DENIED; + } + } + + /** + * @see android.view.inputmethod.InputMethodSubType#getLocate() + */ + @SuppressWarnings("deprecation") + public static String getLocale(InputMethodSubtype inputMethodSubType) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return inputMethodSubType.getLanguageTag(); + } else { + return inputMethodSubType.getLocale(); + } + } + + /** + * Get a URI for |file| which has the image capture. This function assumes that path of |file| + * is based on the result of UiUtils.getDirectoryForImageCapture(). + * + * @param file image capture file. + * @return URI for |file|. + */ + public static Uri getUriForImageCaptureFile(File file) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 + ? ContentUriUtils.getContentUriFromFile(file) + : Uri.fromFile(file); + } + + /** + * Get the URI for a downloaded file. + * + * @param file A downloaded file. + * @return URI for |file|. + */ + public static Uri getUriForDownloadedFile(File file) { + return Build.VERSION.SDK_INT > Build.VERSION_CODES.M + ? FileUtils.getUriForFile(file) + : Uri.fromFile(file); + } + + /** + * @see android.view.Window#FEATURE_INDETERMINATE_PROGRESS + */ + public static void setWindowIndeterminateProgress(Window window) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + @SuppressWarnings("deprecation") + int featureNumber = Window.FEATURE_INDETERMINATE_PROGRESS; + + @SuppressWarnings("deprecation") + int featureValue = Window.PROGRESS_VISIBILITY_OFF; + + window.setFeatureInt(featureNumber, featureValue); + } + } + + /** + * @param activity The {@link Activity} to check. + * @return Whether or not {@code activity} is currently in Android N+ multi-window mode. + */ + public static boolean isInMultiWindowMode(Activity activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return false; + } + return activity.isInMultiWindowMode(); + } + + /** + * Disables the Smart Select {@link TextClassifier} for the given {@link TextView} instance. + * @param textView The {@link TextView} that should have its classifier disabled. + */ + @TargetApi(Build.VERSION_CODES.O) + public static void disableSmartSelectionTextClassifier(TextView textView) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; + + textView.setTextClassifier(TextClassifier.NO_OP); + } + + /** + * Creates an ActivityOptions Bundle with basic options and the LaunchDisplayId set. + * @param displayId The id of the display to launch into. + * @return The created bundle, or null if unsupported. + */ + public static Bundle createLaunchDisplayIdActivityOptions(int displayId) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null; + + ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(displayId); + return options.toBundle(); + } +} diff --git a/base/android/java/src/org/chromium/base/ApkAssets.java b/base/android/java/src/org/chromium/base/ApkAssets.java new file mode 100644 index 0000000000..19108e5957 --- /dev/null +++ b/base/android/java/src/org/chromium/base/ApkAssets.java @@ -0,0 +1,58 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.util.Log; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; + +import java.io.IOException; + +/** + * A utility class to retrieve references to uncompressed assets insides the apk. A reference is + * defined as tuple (file descriptor, offset, size) enabling direct mapping without deflation. + * This can be used even within the renderer process, since it just dup's the apk's fd. + */ +@JNINamespace("base::android") +public class ApkAssets { + private static final String LOGTAG = "ApkAssets"; + + @CalledByNative + public static long[] open(String fileName) { + AssetFileDescriptor afd = null; + try { + AssetManager manager = ContextUtils.getApplicationContext().getAssets(); + afd = manager.openNonAssetFd(fileName); + return new long[] {afd.getParcelFileDescriptor().detachFd(), afd.getStartOffset(), + afd.getLength()}; + } catch (IOException e) { + // As a general rule there's no point logging here because the caller should handle + // receiving an fd of -1 sensibly, and the log message is either mirrored later, or + // unwanted (in the case where a missing file is expected), or wanted but will be + // ignored, as most non-fatal logs are. + // It makes sense to log here when the file exists, but is unable to be opened as an fd + // because (for example) it is unexpectedly compressed in an apk. In that case, the log + // message might save someone some time working out what has gone wrong. + // For that reason, we only suppress the message when the exception message doesn't look + // informative (Android framework passes the filename as the message on actual file not + // found, and the empty string also wouldn't give any useful information for debugging). + if (!e.getMessage().equals("") && !e.getMessage().equals(fileName)) { + Log.e(LOGTAG, "Error while loading asset " + fileName + ": " + e); + } + return new long[] {-1, -1, -1}; + } finally { + try { + if (afd != null) { + afd.close(); + } + } catch (IOException e2) { + Log.e(LOGTAG, "Unable to close AssetFileDescriptor", e2); + } + } + } +} diff --git a/base/android/java/src/org/chromium/base/ApplicationStatus.java b/base/android/java/src/org/chromium/base/ApplicationStatus.java new file mode 100644 index 0000000000..9496da8c1e --- /dev/null +++ b/base/android/java/src/org/chromium/base/ApplicationStatus.java @@ -0,0 +1,620 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Application; +import android.app.Application.ActivityLifecycleCallbacks; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.Window; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; + +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Provides information about the current activity's status, and a way + * to register / unregister listeners for state changes. + */ +@JNINamespace("base::android") +public class ApplicationStatus { + private static final String TOOLBAR_CALLBACK_INTERNAL_WRAPPER_CLASS = + "android.support.v7.internal.app.ToolbarActionBar$ToolbarCallbackWrapper"; + // In builds using the --use_unpublished_apis flag, the ToolbarActionBar class name does not + // include the "internal" package. + private static final String TOOLBAR_CALLBACK_WRAPPER_CLASS = + "android.support.v7.app.ToolbarActionBar$ToolbarCallbackWrapper"; + private static final String WINDOW_PROFILER_CALLBACK = + "com.android.tools.profiler.support.event.WindowProfilerCallback"; + + private static class ActivityInfo { + private int mStatus = ActivityState.DESTROYED; + private ObserverList<ActivityStateListener> mListeners = new ObserverList<>(); + + /** + * @return The current {@link ActivityState} of the activity. + */ + @ActivityState + public int getStatus() { + return mStatus; + } + + /** + * @param status The new {@link ActivityState} of the activity. + */ + public void setStatus(@ActivityState int status) { + mStatus = status; + } + + /** + * @return A list of {@link ActivityStateListener}s listening to this activity. + */ + public ObserverList<ActivityStateListener> getListeners() { + return mListeners; + } + } + + static { + // Chrome initializes this only for the main process. This assert aims to try and catch + // usages from GPU / renderers, while still allowing tests. + assert ContextUtils.isMainProcess() + || ContextUtils.getProcessName().contains(":test") + : "Cannot use ApplicationState from process: " + + ContextUtils.getProcessName(); + } + + private static final Object sCurrentApplicationStateLock = new Object(); + + @SuppressLint("SupportAnnotationUsage") + @ApplicationState + // The getStateForApplication() historically returned ApplicationState.HAS_DESTROYED_ACTIVITIES + // when no activity has been observed. + private static Integer sCurrentApplicationState = ApplicationState.HAS_DESTROYED_ACTIVITIES; + + /** Last activity that was shown (or null if none or it was destroyed). */ + @SuppressLint("StaticFieldLeak") + private static Activity sActivity; + + /** A lazily initialized listener that forwards application state changes to native. */ + private static ApplicationStateListener sNativeApplicationStateListener; + + private static boolean sIsInitialized; + + /** + * A map of which observers listen to state changes from which {@link Activity}. + */ + private static final Map<Activity, ActivityInfo> sActivityInfo = new ConcurrentHashMap<>(); + + /** + * A list of observers to be notified when any {@link Activity} has a state change. + */ + private static final ObserverList<ActivityStateListener> sGeneralActivityStateListeners = + new ObserverList<>(); + + /** + * A list of observers to be notified when the visibility state of this {@link Application} + * changes. See {@link #getStateForApplication()}. + */ + private static final ObserverList<ApplicationStateListener> sApplicationStateListeners = + new ObserverList<>(); + + /** + * A list of observers to be notified when the window focus changes. + * See {@link #registerWindowFocusChangedListener}. + */ + private static final ObserverList<WindowFocusChangedListener> sWindowFocusListeners = + new ObserverList<>(); + + /** + * Interface to be implemented by listeners. + */ + public interface ApplicationStateListener { + /** + * Called when the application's state changes. + * @param newState The application state. + */ + void onApplicationStateChange(@ApplicationState int newState); + } + + /** + * Interface to be implemented by listeners. + */ + public interface ActivityStateListener { + /** + * Called when the activity's state changes. + * @param activity The activity that had a state change. + * @param newState New activity state. + */ + void onActivityStateChange(Activity activity, @ActivityState int newState); + } + + /** + * Interface to be implemented by listeners for window focus events. + */ + public interface WindowFocusChangedListener { + /** + * Called when the window focus changes for {@code activity}. + * @param activity The {@link Activity} that has a window focus changed event. + * @param hasFocus Whether or not {@code activity} gained or lost focus. + */ + public void onWindowFocusChanged(Activity activity, boolean hasFocus); + } + + private ApplicationStatus() {} + + /** + * Registers a listener to receive window focus updates on activities in this application. + * @param listener Listener to receive window focus events. + */ + public static void registerWindowFocusChangedListener(WindowFocusChangedListener listener) { + sWindowFocusListeners.addObserver(listener); + } + + /** + * Unregisters a listener from receiving window focus updates on activities in this application. + * @param listener Listener that doesn't want to receive window focus events. + */ + public static void unregisterWindowFocusChangedListener(WindowFocusChangedListener listener) { + sWindowFocusListeners.removeObserver(listener); + } + + /** + * Intercepts calls to an existing Window.Callback. Most invocations are passed on directly + * to the composed Window.Callback but enables intercepting/manipulating others. + * + * This is used to relay window focus changes throughout the app and remedy a bug in the + * appcompat library. + */ + private static class WindowCallbackProxy implements InvocationHandler { + private final Window.Callback mCallback; + private final Activity mActivity; + + public WindowCallbackProxy(Activity activity, Window.Callback callback) { + mCallback = callback; + mActivity = activity; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("onWindowFocusChanged") && args.length == 1 + && args[0] instanceof Boolean) { + onWindowFocusChanged((boolean) args[0]); + return null; + } else { + try { + return method.invoke(mCallback, args); + } catch (InvocationTargetException e) { + // Special-case for when a method is not defined on the underlying + // Window.Callback object. Because we're using a Proxy to forward all method + // calls, this breaks the Android framework's handling for apps built against + // an older SDK. The framework expects an AbstractMethodError but due to + // reflection it becomes wrapped inside an InvocationTargetException. Undo the + // wrapping to signal the framework accordingly. + if (e.getCause() instanceof AbstractMethodError) { + throw e.getCause(); + } + throw e; + } + } + } + + public void onWindowFocusChanged(boolean hasFocus) { + mCallback.onWindowFocusChanged(hasFocus); + + for (WindowFocusChangedListener listener : sWindowFocusListeners) { + listener.onWindowFocusChanged(mActivity, hasFocus); + } + } + } + + /** + * Initializes the activity status for a specified application. + * + * @param application The application whose status you wish to monitor. + */ + public static void initialize(Application application) { + if (sIsInitialized) return; + sIsInitialized = true; + + registerWindowFocusChangedListener(new WindowFocusChangedListener() { + @Override + public void onWindowFocusChanged(Activity activity, boolean hasFocus) { + if (!hasFocus || activity == sActivity) return; + + int state = getStateForActivity(activity); + + if (state != ActivityState.DESTROYED && state != ActivityState.STOPPED) { + sActivity = activity; + } + + // TODO(dtrainor): Notify of active activity change? + } + }); + + application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(final Activity activity, Bundle savedInstanceState) { + onStateChange(activity, ActivityState.CREATED); + Window.Callback callback = activity.getWindow().getCallback(); + activity.getWindow().setCallback((Window.Callback) Proxy.newProxyInstance( + Window.Callback.class.getClassLoader(), new Class[] {Window.Callback.class}, + new ApplicationStatus.WindowCallbackProxy(activity, callback))); + } + + @Override + public void onActivityDestroyed(Activity activity) { + onStateChange(activity, ActivityState.DESTROYED); + checkCallback(activity); + } + + @Override + public void onActivityPaused(Activity activity) { + onStateChange(activity, ActivityState.PAUSED); + checkCallback(activity); + } + + @Override + public void onActivityResumed(Activity activity) { + onStateChange(activity, ActivityState.RESUMED); + checkCallback(activity); + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + checkCallback(activity); + } + + @Override + public void onActivityStarted(Activity activity) { + onStateChange(activity, ActivityState.STARTED); + checkCallback(activity); + } + + @Override + public void onActivityStopped(Activity activity) { + onStateChange(activity, ActivityState.STOPPED); + checkCallback(activity); + } + + private void checkCallback(Activity activity) { + if (BuildConfig.DCHECK_IS_ON) { + Class<? extends Window.Callback> callback = + activity.getWindow().getCallback().getClass(); + assert(Proxy.isProxyClass(callback) + || callback.getName().equals(TOOLBAR_CALLBACK_WRAPPER_CLASS) + || callback.getName().equals(TOOLBAR_CALLBACK_INTERNAL_WRAPPER_CLASS) + || callback.getName().equals(WINDOW_PROFILER_CALLBACK)); + } + } + }); + } + + /** + * Asserts that initialize method has been called. + */ + private static void assertInitialized() { + if (!sIsInitialized) { + throw new IllegalStateException("ApplicationStatus has not been initialized yet."); + } + } + + /** + * Must be called by the main activity when it changes state. + * + * @param activity Current activity. + * @param newState New state value. + */ + private static void onStateChange(Activity activity, @ActivityState int newState) { + if (activity == null) throw new IllegalArgumentException("null activity is not supported"); + + if (sActivity == null + || newState == ActivityState.CREATED + || newState == ActivityState.RESUMED + || newState == ActivityState.STARTED) { + sActivity = activity; + } + + int oldApplicationState = getStateForApplication(); + ActivityInfo info; + + synchronized (sCurrentApplicationStateLock) { + if (newState == ActivityState.CREATED) { + assert !sActivityInfo.containsKey(activity); + sActivityInfo.put(activity, new ActivityInfo()); + } + + info = sActivityInfo.get(activity); + info.setStatus(newState); + + // Remove before calling listeners so that isEveryActivityDestroyed() returns false when + // this was the last activity. + if (newState == ActivityState.DESTROYED) { + sActivityInfo.remove(activity); + if (activity == sActivity) sActivity = null; + } + + sCurrentApplicationState = determineApplicationState(); + } + + // Notify all state observers that are specifically listening to this activity. + for (ActivityStateListener listener : info.getListeners()) { + listener.onActivityStateChange(activity, newState); + } + + // Notify all state observers that are listening globally for all activity state + // changes. + for (ActivityStateListener listener : sGeneralActivityStateListeners) { + listener.onActivityStateChange(activity, newState); + } + + int applicationState = getStateForApplication(); + if (applicationState != oldApplicationState) { + for (ApplicationStateListener listener : sApplicationStateListeners) { + listener.onApplicationStateChange(applicationState); + } + } + } + + /** + * Testing method to update the state of the specified activity. + */ + @VisibleForTesting + public static void onStateChangeForTesting(Activity activity, int newState) { + onStateChange(activity, newState); + } + + /** + * @return The most recent focused {@link Activity} tracked by this class. Being focused means + * out of all the activities tracked here, it has most recently gained window focus. + */ + public static Activity getLastTrackedFocusedActivity() { + return sActivity; + } + + /** + * @return A {@link List} of all non-destroyed {@link Activity}s. + */ + public static List<WeakReference<Activity>> getRunningActivities() { + assertInitialized(); + List<WeakReference<Activity>> activities = new ArrayList<>(); + for (Activity activity : sActivityInfo.keySet()) { + activities.add(new WeakReference<>(activity)); + } + return activities; + } + + /** + * Query the state for a given activity. If the activity is not being tracked, this will + * return {@link ActivityState#DESTROYED}. + * + * <p> + * Please note that Chrome can have multiple activities running simultaneously. Please also + * look at {@link #getStateForApplication()} for more details. + * + * <p> + * When relying on this method, be familiar with the expected life cycle state + * transitions: + * <a href="http://developer.android.com/guide/components/activities.html#Lifecycle"> + * Activity Lifecycle + * </a> + * + * <p> + * During activity transitions (activity B launching in front of activity A), A will completely + * paused before the creation of activity B begins. + * + * <p> + * A basic flow for activity A starting, followed by activity B being opened and then closed: + * <ul> + * <li> -- Starting Activity A -- + * <li> Activity A - ActivityState.CREATED + * <li> Activity A - ActivityState.STARTED + * <li> Activity A - ActivityState.RESUMED + * <li> -- Starting Activity B -- + * <li> Activity A - ActivityState.PAUSED + * <li> Activity B - ActivityState.CREATED + * <li> Activity B - ActivityState.STARTED + * <li> Activity B - ActivityState.RESUMED + * <li> Activity A - ActivityState.STOPPED + * <li> -- Closing Activity B, Activity A regaining focus -- + * <li> Activity B - ActivityState.PAUSED + * <li> Activity A - ActivityState.STARTED + * <li> Activity A - ActivityState.RESUMED + * <li> Activity B - ActivityState.STOPPED + * <li> Activity B - ActivityState.DESTROYED + * </ul> + * + * @param activity The activity whose state is to be returned. + * @return The state of the specified activity (see {@link ActivityState}). + */ + @ActivityState + public static int getStateForActivity(@Nullable Activity activity) { + ApplicationStatus.assertInitialized(); + if (activity == null) return ActivityState.DESTROYED; + ActivityInfo info = sActivityInfo.get(activity); + return info != null ? info.getStatus() : ActivityState.DESTROYED; + } + + /** + * @return The state of the application (see {@link ApplicationState}). + */ + @ApplicationState + @CalledByNative + public static int getStateForApplication() { + synchronized (sCurrentApplicationStateLock) { + return sCurrentApplicationState; + } + } + + /** + * Checks whether or not any Activity in this Application is visible to the user. Note that + * this includes the PAUSED state, which can happen when the Activity is temporarily covered + * by another Activity's Fragment (e.g.). + * @return Whether any Activity under this Application is visible. + */ + public static boolean hasVisibleActivities() { + int state = getStateForApplication(); + return state == ApplicationState.HAS_RUNNING_ACTIVITIES + || state == ApplicationState.HAS_PAUSED_ACTIVITIES; + } + + /** + * Checks to see if there are any active Activity instances being watched by ApplicationStatus. + * @return True if all Activities have been destroyed. + */ + public static boolean isEveryActivityDestroyed() { + return sActivityInfo.isEmpty(); + } + + /** + * Registers the given listener to receive state changes for all activities. + * @param listener Listener to receive state changes. + */ + public static void registerStateListenerForAllActivities(ActivityStateListener listener) { + sGeneralActivityStateListeners.addObserver(listener); + } + + /** + * Registers the given listener to receive state changes for {@code activity}. After a call to + * {@link ActivityStateListener#onActivityStateChange(Activity, int)} with + * {@link ActivityState#DESTROYED} all listeners associated with that particular + * {@link Activity} are removed. + * @param listener Listener to receive state changes. + * @param activity Activity to track or {@code null} to track all activities. + */ + @SuppressLint("NewApi") + public static void registerStateListenerForActivity(ActivityStateListener listener, + Activity activity) { + if (activity == null) { + throw new IllegalStateException("Attempting to register listener on a null activity."); + } + ApplicationStatus.assertInitialized(); + + ActivityInfo info = sActivityInfo.get(activity); + if (info == null) { + throw new IllegalStateException( + "Attempting to register listener on an untracked activity."); + } + assert info.getStatus() != ActivityState.DESTROYED; + info.getListeners().addObserver(listener); + } + + /** + * Unregisters the given listener from receiving activity state changes. + * @param listener Listener that doesn't want to receive state changes. + */ + public static void unregisterActivityStateListener(ActivityStateListener listener) { + sGeneralActivityStateListeners.removeObserver(listener); + + // Loop through all observer lists for all activities and remove the listener. + for (ActivityInfo info : sActivityInfo.values()) { + info.getListeners().removeObserver(listener); + } + } + + /** + * Registers the given listener to receive state changes for the application. + * @param listener Listener to receive state state changes. + */ + public static void registerApplicationStateListener(ApplicationStateListener listener) { + sApplicationStateListeners.addObserver(listener); + } + + /** + * Unregisters the given listener from receiving state changes. + * @param listener Listener that doesn't want to receive state changes. + */ + public static void unregisterApplicationStateListener(ApplicationStateListener listener) { + sApplicationStateListeners.removeObserver(listener); + } + + /** + * Robolectric JUnit tests create a new application between each test, while all the context + * in static classes isn't reset. This function allows to reset the application status to avoid + * being in a dirty state. + */ + public static void destroyForJUnitTests() { + sApplicationStateListeners.clear(); + sGeneralActivityStateListeners.clear(); + sActivityInfo.clear(); + sWindowFocusListeners.clear(); + sIsInitialized = false; + synchronized (sCurrentApplicationStateLock) { + sCurrentApplicationState = determineApplicationState(); + } + sActivity = null; + sNativeApplicationStateListener = null; + } + + /** + * Registers the single thread-safe native activity status listener. + * This handles the case where the caller is not on the main thread. + * Note that this is used by a leaky singleton object from the native + * side, hence lifecycle management is greatly simplified. + */ + @CalledByNative + private static void registerThreadSafeNativeApplicationStateListener() { + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + if (sNativeApplicationStateListener != null) return; + + sNativeApplicationStateListener = new ApplicationStateListener() { + @Override + public void onApplicationStateChange(int newState) { + nativeOnApplicationStateChange(newState); + } + }; + registerApplicationStateListener(sNativeApplicationStateListener); + } + }); + } + + /** + * Determines the current application state as defined by {@link ApplicationState}. This will + * loop over all the activities and check their state to determine what the general application + * state should be. + * @return HAS_RUNNING_ACTIVITIES if any activity is not paused, stopped, or destroyed. + * HAS_PAUSED_ACTIVITIES if none are running and one is paused. + * HAS_STOPPED_ACTIVITIES if none are running/paused and one is stopped. + * HAS_DESTROYED_ACTIVITIES if none are running/paused/stopped. + */ + @ApplicationState + private static int determineApplicationState() { + boolean hasPausedActivity = false; + boolean hasStoppedActivity = false; + + for (ActivityInfo info : sActivityInfo.values()) { + int state = info.getStatus(); + if (state != ActivityState.PAUSED + && state != ActivityState.STOPPED + && state != ActivityState.DESTROYED) { + return ApplicationState.HAS_RUNNING_ACTIVITIES; + } else if (state == ActivityState.PAUSED) { + hasPausedActivity = true; + } else if (state == ActivityState.STOPPED) { + hasStoppedActivity = true; + } + } + + if (hasPausedActivity) return ApplicationState.HAS_PAUSED_ACTIVITIES; + if (hasStoppedActivity) return ApplicationState.HAS_STOPPED_ACTIVITIES; + return ApplicationState.HAS_DESTROYED_ACTIVITIES; + } + + // Called to notify the native side of state changes. + // IMPORTANT: This is always called on the main thread! + private static native void nativeOnApplicationStateChange(@ApplicationState int newState); +} diff --git a/base/android/java/src/org/chromium/base/BaseSwitches.java b/base/android/java/src/org/chromium/base/BaseSwitches.java new file mode 100644 index 0000000000..fe47cdda1d --- /dev/null +++ b/base/android/java/src/org/chromium/base/BaseSwitches.java @@ -0,0 +1,32 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +/** + * Contains all of the command line switches that are specific to the base/ + * portion of Chromium on Android. + */ +public abstract class BaseSwitches { + // Block ChildProcessMain thread of render process service until a Java debugger is attached. + // To pause even earlier: am set-debug-app org.chromium.chrome:sandbox_process0 + // However, this flag is convenient when you don't know the process number, or want + // all renderers to pause (set-debug-app applies to only one process at a time). + public static final String RENDERER_WAIT_FOR_JAVA_DEBUGGER = "renderer-wait-for-java-debugger"; + + // Force low-end device mode when set. + public static final String ENABLE_LOW_END_DEVICE_MODE = "enable-low-end-device-mode"; + + // Force disabling of low-end device mode when set. + public static final String DISABLE_LOW_END_DEVICE_MODE = "disable-low-end-device-mode"; + + // Adds additional thread idle time information into the trace event output. + public static final String ENABLE_IDLE_TRACING = "enable-idle-tracing"; + + // Default country code to be used for search engine localization. + public static final String DEFAULT_COUNTRY_CODE_AT_INSTALL = "default-country-code"; + + // Prevent instantiation. + private BaseSwitches() {} +} diff --git a/base/android/java/src/org/chromium/base/Callback.java b/base/android/java/src/org/chromium/base/Callback.java new file mode 100644 index 0000000000..f5f20b9c75 --- /dev/null +++ b/base/android/java/src/org/chromium/base/Callback.java @@ -0,0 +1,43 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import org.chromium.base.annotations.CalledByNative; + +/** + * A simple single-argument callback to handle the result of a computation. + * + * @param <T> The type of the computation's result. + */ +public interface Callback<T> { + /** + * Invoked with the result of a computation. + */ + void onResult(T result); + + /** + * JNI Generator does not know how to target static methods on interfaces + * (which is new in Java 8, and requires desugaring). + */ + abstract class Helper { + @SuppressWarnings("unchecked") + @CalledByNative("Helper") + static void onObjectResultFromNative(Callback callback, Object result) { + callback.onResult(result); + } + + @SuppressWarnings("unchecked") + @CalledByNative("Helper") + static void onBooleanResultFromNative(Callback callback, boolean result) { + callback.onResult(Boolean.valueOf(result)); + } + + @SuppressWarnings("unchecked") + @CalledByNative("Helper") + static void onIntResultFromNative(Callback callback, int result) { + callback.onResult(Integer.valueOf(result)); + } + } +} diff --git a/base/android/java/src/org/chromium/base/CollectionUtil.java b/base/android/java/src/org/chromium/base/CollectionUtil.java new file mode 100644 index 0000000000..60933807b8 --- /dev/null +++ b/base/android/java/src/org/chromium/base/CollectionUtil.java @@ -0,0 +1,99 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.support.annotation.NonNull; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Functions used for easier initialization of Java collections. Inspired by + * functionality in com.google.common.collect in Guava but cherry-picked to + * bare-minimum functionality to avoid bloat. (http://crbug.com/272790 provides + * further details) + */ +public final class CollectionUtil { + private CollectionUtil() {} + + @SafeVarargs + public static <E> HashSet<E> newHashSet(E... elements) { + HashSet<E> set = new HashSet<E>(elements.length); + Collections.addAll(set, elements); + return set; + } + + @SafeVarargs + public static <E> ArrayList<E> newArrayList(E... elements) { + ArrayList<E> list = new ArrayList<E>(elements.length); + Collections.addAll(list, elements); + return list; + } + + @VisibleForTesting + public static <E> ArrayList<E> newArrayList(Iterable<E> iterable) { + ArrayList<E> list = new ArrayList<E>(); + for (E element : iterable) { + list.add(element); + } + return list; + } + + @SafeVarargs + public static <K, V> HashMap<K, V> newHashMap(Pair<? extends K, ? extends V>... entries) { + HashMap<K, V> map = new HashMap<>(); + for (Pair<? extends K, ? extends V> entry : entries) { + map.put(entry.first, entry.second); + } + return map; + } + + public static boolean[] booleanListToBooleanArray(@NonNull List<Boolean> list) { + boolean[] array = new boolean[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + public static int[] integerListToIntArray(@NonNull List<Integer> list) { + int[] array = new int[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + public static long[] longListToLongArray(@NonNull List<Long> list) { + long[] array = new long[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + // This is a utility helper method that adds functionality available in API 24 (see + // Collection.forEach). + public static <T> void forEach(Collection<? extends T> collection, Callback<T> worker) { + for (T entry : collection) worker.onResult(entry); + } + + // This is a utility helper method that adds functionality available in API 24 (see + // Collection.forEach). + @SuppressWarnings("unchecked") + public static <K, V> void forEach( + Map<? extends K, ? extends V> map, Callback<Entry<K, V>> worker) { + for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) { + worker.onResult((Map.Entry<K, V>) entry); + } + } +} diff --git a/base/android/java/src/org/chromium/base/CommandLine.java b/base/android/java/src/org/chromium/base/CommandLine.java new file mode 100644 index 0000000000..963b1464af --- /dev/null +++ b/base/android/java/src/org/chromium/base/CommandLine.java @@ -0,0 +1,389 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import org.chromium.base.annotations.MainDex; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Java mirror of base/command_line.h. + * Android applications don't have command line arguments. Instead, they're "simulated" by reading a + * file at a specific location early during startup. Applications each define their own files, e.g., + * ContentShellApplication.COMMAND_LINE_FILE. +**/ +@MainDex +public abstract class CommandLine { + // Public abstract interface, implemented in derived classes. + // All these methods reflect their native-side counterparts. + /** + * Returns true if this command line contains the given switch. + * (Switch names ARE case-sensitive). + */ + @VisibleForTesting + public abstract boolean hasSwitch(String switchString); + + /** + * Return the value associated with the given switch, or null. + * @param switchString The switch key to lookup. It should NOT start with '--' ! + * @return switch value, or null if the switch is not set or set to empty. + */ + public abstract String getSwitchValue(String switchString); + + /** + * Return the value associated with the given switch, or {@code defaultValue} if the switch + * was not specified. + * @param switchString The switch key to lookup. It should NOT start with '--' ! + * @param defaultValue The default value to return if the switch isn't set. + * @return Switch value, or {@code defaultValue} if the switch is not set or set to empty. + */ + public String getSwitchValue(String switchString, String defaultValue) { + String value = getSwitchValue(switchString); + return TextUtils.isEmpty(value) ? defaultValue : value; + } + + /** + * Append a switch to the command line. There is no guarantee + * this action happens before the switch is needed. + * @param switchString the switch to add. It should NOT start with '--' ! + */ + @VisibleForTesting + public abstract void appendSwitch(String switchString); + + /** + * Append a switch and value to the command line. There is no + * guarantee this action happens before the switch is needed. + * @param switchString the switch to add. It should NOT start with '--' ! + * @param value the value for this switch. + * For example, --foo=bar becomes 'foo', 'bar'. + */ + public abstract void appendSwitchWithValue(String switchString, String value); + + /** + * Append switch/value items in "command line" format (excluding argv[0] program name). + * E.g. { '--gofast', '--username=fred' } + * @param array an array of switch or switch/value items in command line format. + * Unlike the other append routines, these switches SHOULD start with '--' . + * Unlike init(), this does not include the program name in array[0]. + */ + public abstract void appendSwitchesAndArguments(String[] array); + + /** + * Determine if the command line is bound to the native (JNI) implementation. + * @return true if the underlying implementation is delegating to the native command line. + */ + public boolean isNativeImplementation() { + return false; + } + + /** + * Returns the switches and arguments passed into the program, with switches and their + * values coming before all of the arguments. + */ + protected abstract String[] getCommandLineArguments(); + + /** + * Destroy the command line. Called when a different instance is set. + * @see #setInstance + */ + protected void destroy() {} + + private static final AtomicReference<CommandLine> sCommandLine = + new AtomicReference<CommandLine>(); + + /** + * @return true if the command line has already been initialized. + */ + public static boolean isInitialized() { + return sCommandLine.get() != null; + } + + // Equivalent to CommandLine::ForCurrentProcess in C++. + @VisibleForTesting + public static CommandLine getInstance() { + CommandLine commandLine = sCommandLine.get(); + assert commandLine != null; + return commandLine; + } + + /** + * Initialize the singleton instance, must be called exactly once (either directly or + * via one of the convenience wrappers below) before using the static singleton instance. + * @param args command line flags in 'argv' format: args[0] is the program name. + */ + public static void init(@Nullable String[] args) { + setInstance(new JavaCommandLine(args)); + } + + /** + * Initialize the command line from the command-line file. + * + * @param file The fully qualified command line file. + */ + public static void initFromFile(String file) { + char[] buffer = readFileAsUtf8(file); + init(buffer == null ? null : tokenizeQuotedArguments(buffer)); + } + + /** + * Resets both the java proxy and the native command lines. This allows the entire + * command line initialization to be re-run including the call to onJniLoaded. + */ + @VisibleForTesting + public static void reset() { + setInstance(null); + } + + /** + * Parse command line flags from a flat buffer, supporting double-quote enclosed strings + * containing whitespace. argv elements are derived by splitting the buffer on whitepace; + * double quote characters may enclose tokens containing whitespace; a double-quote literal + * may be escaped with back-slash. (Otherwise backslash is taken as a literal). + * @param buffer A command line in command line file format as described above. + * @return the tokenized arguments, suitable for passing to init(). + */ + @VisibleForTesting + static String[] tokenizeQuotedArguments(char[] buffer) { + // Just field trials can take up to 10K of command line. + if (buffer.length > 64 * 1024) { + // Check that our test runners are setting a reasonable number of flags. + throw new RuntimeException("Flags file too big: " + buffer.length); + } + + ArrayList<String> args = new ArrayList<String>(); + StringBuilder arg = null; + final char noQuote = '\0'; + final char singleQuote = '\''; + final char doubleQuote = '"'; + char currentQuote = noQuote; + for (char c : buffer) { + // Detect start or end of quote block. + if ((currentQuote == noQuote && (c == singleQuote || c == doubleQuote)) + || c == currentQuote) { + if (arg != null && arg.length() > 0 && arg.charAt(arg.length() - 1) == '\\') { + // Last char was a backslash; pop it, and treat c as a literal. + arg.setCharAt(arg.length() - 1, c); + } else { + currentQuote = currentQuote == noQuote ? c : noQuote; + } + } else if (currentQuote == noQuote && Character.isWhitespace(c)) { + if (arg != null) { + args.add(arg.toString()); + arg = null; + } + } else { + if (arg == null) arg = new StringBuilder(); + arg.append(c); + } + } + if (arg != null) { + if (currentQuote != noQuote) { + Log.w(TAG, "Unterminated quoted string: " + arg); + } + args.add(arg.toString()); + } + return args.toArray(new String[args.size()]); + } + + private static final String TAG = "CommandLine"; + private static final String SWITCH_PREFIX = "--"; + private static final String SWITCH_TERMINATOR = SWITCH_PREFIX; + private static final String SWITCH_VALUE_SEPARATOR = "="; + + public static void enableNativeProxy() { + // Make a best-effort to ensure we make a clean (atomic) switch over from the old to + // the new command line implementation. If another thread is modifying the command line + // when this happens, all bets are off. (As per the native CommandLine). + sCommandLine.set(new NativeCommandLine(getJavaSwitchesOrNull())); + } + + @Nullable + public static String[] getJavaSwitchesOrNull() { + CommandLine commandLine = sCommandLine.get(); + if (commandLine != null) { + return commandLine.getCommandLineArguments(); + } + return null; + } + + private static void setInstance(CommandLine commandLine) { + CommandLine oldCommandLine = sCommandLine.getAndSet(commandLine); + if (oldCommandLine != null) { + oldCommandLine.destroy(); + } + } + + /** + * @param fileName the file to read in. + * @return Array of chars read from the file, or null if the file cannot be read. + */ + private static char[] readFileAsUtf8(String fileName) { + File f = new File(fileName); + try (FileReader reader = new FileReader(f)) { + char[] buffer = new char[(int) f.length()]; + int charsRead = reader.read(buffer); + // charsRead < f.length() in the case of multibyte characters. + return Arrays.copyOfRange(buffer, 0, charsRead); + } catch (IOException e) { + return null; // Most likely file not found. + } + } + + private CommandLine() {} + + private static class JavaCommandLine extends CommandLine { + private HashMap<String, String> mSwitches = new HashMap<String, String>(); + private ArrayList<String> mArgs = new ArrayList<String>(); + + // The arguments begin at index 1, since index 0 contains the executable name. + private int mArgsBegin = 1; + + JavaCommandLine(@Nullable String[] args) { + if (args == null || args.length == 0 || args[0] == null) { + mArgs.add(""); + } else { + mArgs.add(args[0]); + appendSwitchesInternal(args, 1); + } + // Invariant: we always have the argv[0] program name element. + assert mArgs.size() > 0; + } + + @Override + protected String[] getCommandLineArguments() { + return mArgs.toArray(new String[mArgs.size()]); + } + + @Override + public boolean hasSwitch(String switchString) { + return mSwitches.containsKey(switchString); + } + + @Override + public String getSwitchValue(String switchString) { + // This is slightly round about, but needed for consistency with the NativeCommandLine + // version which does not distinguish empty values from key not present. + String value = mSwitches.get(switchString); + return value == null || value.isEmpty() ? null : value; + } + + @Override + public void appendSwitch(String switchString) { + appendSwitchWithValue(switchString, null); + } + + /** + * Appends a switch to the current list. + * @param switchString the switch to add. It should NOT start with '--' ! + * @param value the value for this switch. + */ + @Override + public void appendSwitchWithValue(String switchString, String value) { + mSwitches.put(switchString, value == null ? "" : value); + + // Append the switch and update the switches/arguments divider mArgsBegin. + String combinedSwitchString = SWITCH_PREFIX + switchString; + if (value != null && !value.isEmpty()) { + combinedSwitchString += SWITCH_VALUE_SEPARATOR + value; + } + + mArgs.add(mArgsBegin++, combinedSwitchString); + } + + @Override + public void appendSwitchesAndArguments(String[] array) { + appendSwitchesInternal(array, 0); + } + + // Add the specified arguments, but skipping the first |skipCount| elements. + private void appendSwitchesInternal(String[] array, int skipCount) { + boolean parseSwitches = true; + for (String arg : array) { + if (skipCount > 0) { + --skipCount; + continue; + } + + if (arg.equals(SWITCH_TERMINATOR)) { + parseSwitches = false; + } + + if (parseSwitches && arg.startsWith(SWITCH_PREFIX)) { + String[] parts = arg.split(SWITCH_VALUE_SEPARATOR, 2); + String value = parts.length > 1 ? parts[1] : null; + appendSwitchWithValue(parts[0].substring(SWITCH_PREFIX.length()), value); + } else { + mArgs.add(arg); + } + } + } + } + + private static class NativeCommandLine extends CommandLine { + public NativeCommandLine(@Nullable String[] args) { + nativeInit(args); + } + + @Override + public boolean hasSwitch(String switchString) { + return nativeHasSwitch(switchString); + } + + @Override + public String getSwitchValue(String switchString) { + return nativeGetSwitchValue(switchString); + } + + @Override + public void appendSwitch(String switchString) { + nativeAppendSwitch(switchString); + } + + @Override + public void appendSwitchWithValue(String switchString, String value) { + nativeAppendSwitchWithValue(switchString, value); + } + + @Override + public void appendSwitchesAndArguments(String[] array) { + nativeAppendSwitchesAndArguments(array); + } + + @Override + public boolean isNativeImplementation() { + return true; + } + + @Override + protected String[] getCommandLineArguments() { + assert false; + return null; + } + + @Override + protected void destroy() { + // TODO(https://crbug.com/771205): Downgrade this to an assert once we have eliminated + // tests that do this. + throw new IllegalStateException("Can't destroy native command line after startup"); + } + } + + private static native void nativeInit(String[] args); + private static native boolean nativeHasSwitch(String switchString); + private static native String nativeGetSwitchValue(String switchString); + private static native void nativeAppendSwitch(String switchString); + private static native void nativeAppendSwitchWithValue(String switchString, String value); + private static native void nativeAppendSwitchesAndArguments(String[] array); +} diff --git a/base/android/java/src/org/chromium/base/CommandLineInitUtil.java b/base/android/java/src/org/chromium/base/CommandLineInitUtil.java new file mode 100644 index 0000000000..e51b95d6b5 --- /dev/null +++ b/base/android/java/src/org/chromium/base/CommandLineInitUtil.java @@ -0,0 +1,103 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.provider.Settings; +import android.support.annotation.Nullable; + +import java.io.File; + +/** + * Provides implementation of command line initialization for Android. + */ +public final class CommandLineInitUtil { + /** + * The location of the command line file needs to be in a protected + * directory so requires root access to be tweaked, i.e., no other app in a + * regular (non-rooted) device can change this file's contents. + * See below for debugging on a regular (non-rooted) device. + */ + private static final String COMMAND_LINE_FILE_PATH = "/data/local"; + + /** + * This path (writable by the shell in regular non-rooted "user" builds) is used when: + * 1) The "debug app" is set to the application calling this. + * and + * 2) ADB is enabled. + * 3) Force enabled by the embedder. + */ + private static final String COMMAND_LINE_FILE_PATH_DEBUG_APP = "/data/local/tmp"; + + private CommandLineInitUtil() { + } + + /** + * Initializes the CommandLine class, pulling command line arguments from {@code fileName}. + * @param fileName The name of the command line file to pull arguments from. + */ + public static void initCommandLine(String fileName) { + initCommandLine(fileName, null); + } + + /** + * Initializes the CommandLine class, pulling command line arguments from {@code fileName}. + * @param fileName The name of the command line file to pull arguments from. + * @param shouldUseDebugFlags If non-null, returns whether debug flags are allowed to be used. + */ + public static void initCommandLine( + String fileName, @Nullable Supplier<Boolean> shouldUseDebugFlags) { + assert !CommandLine.isInitialized(); + File commandLineFile = new File(COMMAND_LINE_FILE_PATH_DEBUG_APP, fileName); + // shouldUseDebugCommandLine() uses IPC, so don't bother calling it if no flags file exists. + boolean debugFlagsExist = commandLineFile.exists(); + if (!debugFlagsExist || !shouldUseDebugCommandLine(shouldUseDebugFlags)) { + commandLineFile = new File(COMMAND_LINE_FILE_PATH, fileName); + } + CommandLine.initFromFile(commandLineFile.getPath()); + } + + /** + * Use an alternative path if: + * - The current build is "eng" or "userdebug", OR + * - adb is enabled and this is the debug app, OR + * - Force enabled by the embedder. + * @param shouldUseDebugFlags If non-null, returns whether debug flags are allowed to be used. + */ + private static boolean shouldUseDebugCommandLine( + @Nullable Supplier<Boolean> shouldUseDebugFlags) { + if (shouldUseDebugFlags != null && shouldUseDebugFlags.get()) return true; + Context context = ContextUtils.getApplicationContext(); + String debugApp = Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 + ? getDebugAppPreJBMR1(context) + : getDebugAppJBMR1(context); + // Check isDebugAndroid() last to get full code coverage when using userdebug devices. + return context.getPackageName().equals(debugApp) || BuildInfo.isDebugAndroid(); + } + + @SuppressLint("NewApi") + private static String getDebugAppJBMR1(Context context) { + boolean adbEnabled = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.ADB_ENABLED, 0) == 1; + if (adbEnabled) { + return Settings.Global.getString(context.getContentResolver(), + Settings.Global.DEBUG_APP); + } + return null; + } + + @SuppressWarnings("deprecation") + private static String getDebugAppPreJBMR1(Context context) { + boolean adbEnabled = Settings.System.getInt(context.getContentResolver(), + Settings.System.ADB_ENABLED, 0) == 1; + if (adbEnabled) { + return Settings.System.getString(context.getContentResolver(), + Settings.System.DEBUG_APP); + } + return null; + } +} diff --git a/base/android/java/src/org/chromium/base/ContentUriUtils.java b/base/android/java/src/org/chromium/base/ContentUriUtils.java new file mode 100644 index 0000000000..ba92a56c4f --- /dev/null +++ b/base/android/java/src/org/chromium/base/ContentUriUtils.java @@ -0,0 +1,251 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import org.chromium.base.annotations.CalledByNative; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * This class provides methods to access content URI schemes. + */ +public abstract class ContentUriUtils { + private static final String TAG = "ContentUriUtils"; + private static FileProviderUtil sFileProviderUtil; + + // Guards access to sFileProviderUtil. + private static final Object sLock = new Object(); + + /** + * Provides functionality to translate a file into a content URI for use + * with a content provider. + */ + public interface FileProviderUtil { + /** + * Generate a content URI from the given file. + * + * @param file The file to be translated. + */ + Uri getContentUriFromFile(File file); + } + + // Prevent instantiation. + private ContentUriUtils() {} + + public static void setFileProviderUtil(FileProviderUtil util) { + synchronized (sLock) { + sFileProviderUtil = util; + } + } + + public static Uri getContentUriFromFile(File file) { + synchronized (sLock) { + if (sFileProviderUtil != null) { + return sFileProviderUtil.getContentUriFromFile(file); + } + } + return null; + } + + /** + * Opens the content URI for reading, and returns the file descriptor to + * the caller. The caller is responsible for closing the file descriptor. + * + * @param uriString the content URI to open + * @return file descriptor upon success, or -1 otherwise. + */ + @CalledByNative + public static int openContentUriForRead(String uriString) { + AssetFileDescriptor afd = getAssetFileDescriptor(uriString); + if (afd != null) { + return afd.getParcelFileDescriptor().detachFd(); + } + return -1; + } + + /** + * Check whether a content URI exists. + * + * @param uriString the content URI to query. + * @return true if the URI exists, or false otherwise. + */ + @CalledByNative + public static boolean contentUriExists(String uriString) { + AssetFileDescriptor asf = null; + try { + asf = getAssetFileDescriptor(uriString); + return asf != null; + } finally { + // Do not use StreamUtil.closeQuietly here, as AssetFileDescriptor + // does not implement Closeable until KitKat. + if (asf != null) { + try { + asf.close(); + } catch (IOException e) { + // Closing quietly. + } + } + } + } + + /** + * Retrieve the MIME type for the content URI. + * + * @param uriString the content URI to look up. + * @return MIME type or null if the input params are empty or invalid. + */ + @CalledByNative + public static String getMimeType(String uriString) { + ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver(); + Uri uri = Uri.parse(uriString); + if (isVirtualDocument(uri)) { + String[] streamTypes = resolver.getStreamTypes(uri, "*/*"); + return (streamTypes != null && streamTypes.length > 0) ? streamTypes[0] : null; + } + return resolver.getType(uri); + } + + /** + * Helper method to open a content URI and returns the ParcelFileDescriptor. + * + * @param uriString the content URI to open. + * @return AssetFileDescriptor of the content URI, or NULL if the file does not exist. + */ + private static AssetFileDescriptor getAssetFileDescriptor(String uriString) { + ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver(); + Uri uri = Uri.parse(uriString); + + try { + if (isVirtualDocument(uri)) { + String[] streamTypes = resolver.getStreamTypes(uri, "*/*"); + if (streamTypes != null && streamTypes.length > 0) { + AssetFileDescriptor afd = + resolver.openTypedAssetFileDescriptor(uri, streamTypes[0], null); + if (afd != null && afd.getStartOffset() != 0) { + // Do not use StreamUtil.closeQuietly here, as AssetFileDescriptor + // does not implement Closeable until KitKat. + try { + afd.close(); + } catch (IOException e) { + // Closing quietly. + } + throw new SecurityException("Cannot open files with non-zero offset type."); + } + return afd; + } + } else { + ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); + if (pfd != null) { + return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); + } + } + } catch (FileNotFoundException e) { + Log.w(TAG, "Cannot find content uri: " + uriString, e); + } catch (SecurityException e) { + Log.w(TAG, "Cannot open content uri: " + uriString, e); + } catch (Exception e) { + Log.w(TAG, "Unknown content uri: " + uriString, e); + } + return null; + } + + /** + * Method to resolve the display name of a content URI. + * + * @param uri the content URI to be resolved. + * @param context {@link Context} in interest. + * @param columnField the column field to query. + * @return the display name of the @code uri if present in the database + * or an empty string otherwise. + */ + public static String getDisplayName(Uri uri, Context context, String columnField) { + if (uri == null) return ""; + ContentResolver contentResolver = context.getContentResolver(); + try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) { + if (cursor != null && cursor.getCount() >= 1) { + cursor.moveToFirst(); + int displayNameIndex = cursor.getColumnIndex(columnField); + if (displayNameIndex == -1) { + return ""; + } + String displayName = cursor.getString(displayNameIndex); + // For Virtual documents, try to modify the file extension so it's compatible + // with the alternative MIME type. + if (hasVirtualFlag(cursor)) { + String[] mimeTypes = contentResolver.getStreamTypes(uri, "*/*"); + if (mimeTypes != null && mimeTypes.length > 0) { + String ext = + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeTypes[0]); + if (ext != null) { + // Just append, it's simpler and more secure than altering an + // existing extension. + displayName += "." + ext; + } + } + } + return displayName; + } + } catch (NullPointerException e) { + // Some android models don't handle the provider call correctly. + // see crbug.com/345393 + return ""; + } + return ""; + } + + /** + * Checks whether the passed Uri represents a virtual document. + * + * @param uri the content URI to be resolved. + * @return True for virtual file, false for any other file. + */ + private static boolean isVirtualDocument(Uri uri) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return false; + if (uri == null) return false; + if (!DocumentsContract.isDocumentUri(ContextUtils.getApplicationContext(), uri)) { + return false; + } + ContentResolver contentResolver = ContextUtils.getApplicationContext().getContentResolver(); + try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) { + if (cursor != null && cursor.getCount() >= 1) { + cursor.moveToFirst(); + return hasVirtualFlag(cursor); + } + } catch (NullPointerException e) { + // Some android models don't handle the provider call correctly. + // see crbug.com/345393 + return false; + } + return false; + } + + /** + * Checks whether the passed cursor for a document has a virtual document flag. + * + * The called must close the passed cursor. + * + * @param cursor Cursor with COLUMN_FLAGS. + * @return True for virtual file, false for any other file. + */ + private static boolean hasVirtualFlag(Cursor cursor) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false; + int index = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS); + return index > -1 + && (cursor.getLong(index) & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0; + } +} diff --git a/base/android/java/src/org/chromium/base/CpuFeatures.java b/base/android/java/src/org/chromium/base/CpuFeatures.java new file mode 100644 index 0000000000..ae4969c99e --- /dev/null +++ b/base/android/java/src/org/chromium/base/CpuFeatures.java @@ -0,0 +1,42 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import org.chromium.base.annotations.JNINamespace; + +// The only purpose of this class is to allow sending CPU properties +// from the browser process to sandboxed renderer processes. This is +// needed because sandboxed processes cannot, on ARM, query the kernel +// about the CPU's properties by parsing /proc, so this operation must +// be performed in the browser process, and the result passed to +// renderer ones. +// +// For more context, see http://crbug.com/164154 +// +// Technically, this is a wrapper around the native NDK cpufeatures +// library. The exact CPU features bits are never used in Java so +// there is no point in duplicating their definitions here. +// +@JNINamespace("base::android") +public abstract class CpuFeatures { + /** + * Return the number of CPU Cores on the device. + */ + public static int getCount() { + return nativeGetCoreCount(); + } + + /** + * Return the CPU feature mask. + * This is a 64-bit integer that corresponds to the CPU's features. + * The value comes directly from android_getCpuFeatures(). + */ + public static long getMask() { + return nativeGetCpuFeatures(); + } + + private static native int nativeGetCoreCount(); + private static native long nativeGetCpuFeatures(); +} diff --git a/base/android/java/src/org/chromium/base/EarlyTraceEvent.java b/base/android/java/src/org/chromium/base/EarlyTraceEvent.java new file mode 100644 index 0000000000..0f64fc2329 --- /dev/null +++ b/base/android/java/src/org/chromium/base/EarlyTraceEvent.java @@ -0,0 +1,299 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.os.Process; +import android.os.StrictMode; +import android.os.SystemClock; + +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.MainDex; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Support for early tracing, before the native library is loaded. + * + * This is limited, as: + * - Arguments are not supported + * - Thread time is not reported + * - Two events with the same name cannot be in progress at the same time. + * + * Events recorded here are buffered in Java until the native library is available. Then it waits + * for the completion of pending events, and sends the events to the native side. + * + * Locking: This class is threadsafe. It is enabled when general tracing is, and then disabled when + * tracing is enabled from the native side. Event completions are still processed as long + * as some are pending, then early tracing is permanently disabled after dumping the + * events. This means that if any early event is still pending when tracing is disabled, + * all early events are dropped. + */ +@JNINamespace("base::android") +@MainDex +public class EarlyTraceEvent { + // Must be kept in sync with the native kAndroidTraceConfigFile. + private static final String TRACE_CONFIG_FILENAME = "/data/local/chrome-trace-config.json"; + + /** Single trace event. */ + @VisibleForTesting + static final class Event { + final String mName; + final int mThreadId; + final long mBeginTimeNanos; + final long mBeginThreadTimeMillis; + long mEndTimeNanos; + long mEndThreadTimeMillis; + + Event(String name) { + mName = name; + mThreadId = Process.myTid(); + mBeginTimeNanos = elapsedRealtimeNanos(); + mBeginThreadTimeMillis = SystemClock.currentThreadTimeMillis(); + } + + void end() { + assert mEndTimeNanos == 0; + assert mEndThreadTimeMillis == 0; + mEndTimeNanos = elapsedRealtimeNanos(); + mEndThreadTimeMillis = SystemClock.currentThreadTimeMillis(); + } + + @VisibleForTesting + @SuppressLint("NewApi") + static long elapsedRealtimeNanos() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return SystemClock.elapsedRealtimeNanos(); + } else { + return SystemClock.elapsedRealtime() * 1000000; + } + } + } + + @VisibleForTesting + static final class AsyncEvent { + final boolean mIsStart; + final String mName; + final long mId; + final long mTimestampNanos; + + AsyncEvent(String name, long id, boolean isStart) { + mName = name; + mId = id; + mIsStart = isStart; + mTimestampNanos = Event.elapsedRealtimeNanos(); + } + } + + // State transitions are: + // - enable(): DISABLED -> ENABLED + // - disable(): ENABLED -> FINISHING + // - Once there are no pending events: FINISHING -> FINISHED. + @VisibleForTesting static final int STATE_DISABLED = 0; + @VisibleForTesting static final int STATE_ENABLED = 1; + @VisibleForTesting static final int STATE_FINISHING = 2; + @VisibleForTesting static final int STATE_FINISHED = 3; + + // Locks the fields below. + private static final Object sLock = new Object(); + + @VisibleForTesting static volatile int sState = STATE_DISABLED; + // Not final as these object are not likely to be used at all. + @VisibleForTesting static List<Event> sCompletedEvents; + @VisibleForTesting + static Map<String, Event> sPendingEventByKey; + @VisibleForTesting static List<AsyncEvent> sAsyncEvents; + @VisibleForTesting static List<String> sPendingAsyncEvents; + + /** @see TraceEvent#MaybeEnableEarlyTracing(). + */ + static void maybeEnable() { + ThreadUtils.assertOnUiThread(); + boolean shouldEnable = false; + // Checking for the trace config filename touches the disk. + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); + try { + if (CommandLine.getInstance().hasSwitch("trace-startup")) { + shouldEnable = true; + } else { + try { + shouldEnable = (new File(TRACE_CONFIG_FILENAME)).exists(); + } catch (SecurityException e) { + // Access denied, not enabled. + } + } + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + if (shouldEnable) enable(); + } + + @VisibleForTesting + static void enable() { + synchronized (sLock) { + if (sState != STATE_DISABLED) return; + sCompletedEvents = new ArrayList<Event>(); + sPendingEventByKey = new HashMap<String, Event>(); + sAsyncEvents = new ArrayList<AsyncEvent>(); + sPendingAsyncEvents = new ArrayList<String>(); + sState = STATE_ENABLED; + } + } + + /** + * Disables Early tracing. + * + * Once this is called, no new event will be registered. However, end() calls are still recorded + * as long as there are pending events. Once there are none left, pass the events to the native + * side. + */ + static void disable() { + synchronized (sLock) { + if (!enabled()) return; + sState = STATE_FINISHING; + maybeFinishLocked(); + } + } + + /** + * Returns whether early tracing is currently active. + * + * Active means that Early Tracing is either enabled or waiting to complete pending events. + */ + static boolean isActive() { + int state = sState; + return (state == STATE_ENABLED || state == STATE_FINISHING); + } + + static boolean enabled() { + return sState == STATE_ENABLED; + } + + /** @see {@link TraceEvent#begin()}. */ + public static void begin(String name) { + // begin() and end() are going to be called once per TraceEvent, this avoids entering a + // synchronized block at each and every call. + if (!enabled()) return; + Event event = new Event(name); + Event conflictingEvent; + synchronized (sLock) { + if (!enabled()) return; + conflictingEvent = sPendingEventByKey.put(makeEventKeyForCurrentThread(name), event); + } + if (conflictingEvent != null) { + throw new IllegalArgumentException( + "Multiple pending trace events can't have the same name"); + } + } + + /** @see {@link TraceEvent#end()}. */ + public static void end(String name) { + if (!isActive()) return; + synchronized (sLock) { + if (!isActive()) return; + Event event = sPendingEventByKey.remove(makeEventKeyForCurrentThread(name)); + if (event == null) return; + event.end(); + sCompletedEvents.add(event); + if (sState == STATE_FINISHING) maybeFinishLocked(); + } + } + + /** @see {@link TraceEvent#startAsync()}. */ + public static void startAsync(String name, long id) { + if (!enabled()) return; + AsyncEvent event = new AsyncEvent(name, id, true /*isStart*/); + synchronized (sLock) { + if (!enabled()) return; + sAsyncEvents.add(event); + sPendingAsyncEvents.add(name); + } + } + + /** @see {@link TraceEvent#finishAsync()}. */ + public static void finishAsync(String name, long id) { + if (!isActive()) return; + AsyncEvent event = new AsyncEvent(name, id, false /*isStart*/); + synchronized (sLock) { + if (!isActive()) return; + if (!sPendingAsyncEvents.remove(name)) return; + sAsyncEvents.add(event); + if (sState == STATE_FINISHING) maybeFinishLocked(); + } + } + + @VisibleForTesting + static void resetForTesting() { + sState = EarlyTraceEvent.STATE_DISABLED; + sCompletedEvents = null; + sPendingEventByKey = null; + sAsyncEvents = null; + sPendingAsyncEvents = null; + } + + private static void maybeFinishLocked() { + if (!sCompletedEvents.isEmpty()) { + dumpEvents(sCompletedEvents); + sCompletedEvents.clear(); + } + if (!sAsyncEvents.isEmpty()) { + dumpAsyncEvents(sAsyncEvents); + sAsyncEvents.clear(); + } + if (sPendingEventByKey.isEmpty() && sPendingAsyncEvents.isEmpty()) { + sState = STATE_FINISHED; + sPendingEventByKey = null; + sCompletedEvents = null; + sPendingAsyncEvents = null; + sAsyncEvents = null; + } + } + + private static void dumpEvents(List<Event> events) { + long offsetNanos = getOffsetNanos(); + for (Event e : events) { + nativeRecordEarlyEvent(e.mName, e.mBeginTimeNanos + offsetNanos, + e.mEndTimeNanos + offsetNanos, e.mThreadId, + e.mEndThreadTimeMillis - e.mBeginThreadTimeMillis); + } + } + private static void dumpAsyncEvents(List<AsyncEvent> events) { + long offsetNanos = getOffsetNanos(); + for (AsyncEvent e : events) { + if (e.mIsStart) { + nativeRecordEarlyStartAsyncEvent(e.mName, e.mId, e.mTimestampNanos + offsetNanos); + } else { + nativeRecordEarlyFinishAsyncEvent(e.mName, e.mId, e.mTimestampNanos + offsetNanos); + } + } + } + + private static long getOffsetNanos() { + long nativeNowNanos = TimeUtils.nativeGetTimeTicksNowUs() * 1000; + long javaNowNanos = Event.elapsedRealtimeNanos(); + return nativeNowNanos - javaNowNanos; + } + + /** + * Returns a key which consists of |name| and the ID of the current thread. + * The key is used with pending events making them thread-specific, thus avoiding + * an exception when similarly named events are started from multiple threads. + */ + @VisibleForTesting + static String makeEventKeyForCurrentThread(String name) { + return name + "@" + Process.myTid(); + } + + private static native void nativeRecordEarlyEvent(String name, long beginTimNanos, + long endTimeNanos, int threadId, long threadDurationMillis); + private static native void nativeRecordEarlyStartAsyncEvent( + String name, long id, long timestamp); + private static native void nativeRecordEarlyFinishAsyncEvent( + String name, long id, long timestamp); +} diff --git a/base/android/java/src/org/chromium/base/EventLog.java b/base/android/java/src/org/chromium/base/EventLog.java new file mode 100644 index 0000000000..f889175b7a --- /dev/null +++ b/base/android/java/src/org/chromium/base/EventLog.java @@ -0,0 +1,20 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; + +/** + * A simple interface to Android's EventLog to be used by native code. + */ +@JNINamespace("base::android") +public class EventLog { + + @CalledByNative + public static void writeEvent(int tag, int value) { + android.util.EventLog.writeEvent(tag, value); + } +} diff --git a/base/android/java/src/org/chromium/base/FieldTrialList.java b/base/android/java/src/org/chromium/base/FieldTrialList.java new file mode 100644 index 0000000000..c3468a4af0 --- /dev/null +++ b/base/android/java/src/org/chromium/base/FieldTrialList.java @@ -0,0 +1,46 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import org.chromium.base.annotations.MainDex; + +/** + * Helper to get field trial information. + */ +@MainDex +public class FieldTrialList { + + private FieldTrialList() {} + + /** + * @param trialName The name of the trial to get the group for. + * @return The group name chosen for the named trial, or the empty string if the trial does + * not exist. + */ + public static String findFullName(String trialName) { + return nativeFindFullName(trialName); + } + + /** + * @param trialName The name of the trial to get the group for. + * @return Whether the trial exists or not. + */ + public static boolean trialExists(String trialName) { + return nativeTrialExists(trialName); + } + + /** + * @param trialName The name of the trial with the parameter. + * @param parameterKey The key of the parameter. + * @return The value of the parameter or an empty string if not found. + */ + public static String getVariationParameter(String trialName, String parameterKey) { + return nativeGetVariationParameter(trialName, parameterKey); + } + + private static native String nativeFindFullName(String trialName); + private static native boolean nativeTrialExists(String trialName); + private static native String nativeGetVariationParameter(String trialName, String parameterKey); +} diff --git a/base/android/java/src/org/chromium/base/FileUtils.java b/base/android/java/src/org/chromium/base/FileUtils.java new file mode 100644 index 0000000000..e44cd928ae --- /dev/null +++ b/base/android/java/src/org/chromium/base/FileUtils.java @@ -0,0 +1,149 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.content.Context; +import android.net.Uri; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Locale; + +/** + * Helper methods for dealing with Files. + */ +public class FileUtils { + private static final String TAG = "FileUtils"; + + /** + * Delete the given File and (if it's a directory) everything within it. + */ + public static void recursivelyDeleteFile(File currentFile) { + ThreadUtils.assertOnBackgroundThread(); + if (currentFile.isDirectory()) { + File[] files = currentFile.listFiles(); + if (files != null) { + for (File file : files) { + recursivelyDeleteFile(file); + } + } + } + + if (!currentFile.delete()) Log.e(TAG, "Failed to delete: " + currentFile); + } + + /** + * Delete the given files or directories by calling {@link #recursivelyDeleteFile(File)}. + * @param files The files to delete. + */ + public static void batchDeleteFiles(List<File> files) { + ThreadUtils.assertOnBackgroundThread(); + + for (File file : files) { + if (file.exists()) recursivelyDeleteFile(file); + } + } + + /** + * Extracts an asset from the app's APK to a file. + * @param context + * @param assetName Name of the asset to extract. + * @param dest File to extract the asset to. + * @return true on success. + */ + public static boolean extractAsset(Context context, String assetName, File dest) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = context.getAssets().open(assetName); + outputStream = new BufferedOutputStream(new FileOutputStream(dest)); + byte[] buffer = new byte[8192]; + int c; + while ((c = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, c); + } + inputStream.close(); + outputStream.close(); + return true; + } catch (IOException e) { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ex) { + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException ex) { + } + } + } + return false; + } + + /** + * Atomically copies the data from an input stream into an output file. + * @param is Input file stream to read data from. + * @param outFile Output file path. + * @param buffer Caller-provided buffer. Provided to avoid allocating the same + * buffer on each call when copying several files in sequence. + * @throws IOException in case of I/O error. + */ + public static void copyFileStreamAtomicWithBuffer(InputStream is, File outFile, byte[] buffer) + throws IOException { + File tmpOutputFile = new File(outFile.getPath() + ".tmp"); + try (OutputStream os = new FileOutputStream(tmpOutputFile)) { + Log.i(TAG, "Writing to %s", outFile); + + int count = 0; + while ((count = is.read(buffer, 0, buffer.length)) != -1) { + os.write(buffer, 0, count); + } + } + if (!tmpOutputFile.renameTo(outFile)) { + throw new IOException(); + } + } + + /** + * Returns a URI that points at the file. + * @param file File to get a URI for. + * @return URI that points at that file, either as a content:// URI or a file:// URI. + */ + public static Uri getUriForFile(File file) { + // TODO(crbug/709584): Uncomment this when http://crbug.com/709584 has been fixed. + // assert !ThreadUtils.runningOnUiThread(); + Uri uri = null; + + try { + // Try to obtain a content:// URI, which is preferred to a file:// URI so that + // receiving apps don't attempt to determine the file's mime type (which often fails). + uri = ContentUriUtils.getContentUriFromFile(file); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not create content uri: " + e); + } + + if (uri == null) uri = Uri.fromFile(file); + + return uri; + } + + /** + * Returns the file extension, or an empty string if none. + * @param file Name of the file, with or without the full path. + * @return empty string if no extension, extension otherwise. + */ + public static String getExtension(String file) { + int index = file.lastIndexOf('.'); + if (index == -1) return ""; + return file.substring(index + 1).toLowerCase(Locale.US); + } +} diff --git a/base/android/java/src/org/chromium/base/ImportantFileWriterAndroid.java b/base/android/java/src/org/chromium/base/ImportantFileWriterAndroid.java new file mode 100644 index 0000000000..cbaf7f76a1 --- /dev/null +++ b/base/android/java/src/org/chromium/base/ImportantFileWriterAndroid.java @@ -0,0 +1,31 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import org.chromium.base.annotations.JNINamespace; + +/** + * This class provides an interface to the native class for writing + * important data files without risking data loss. + */ +@JNINamespace("base::android") +public class ImportantFileWriterAndroid { + + /** + * Write a binary file atomically. + * + * This either writes all the data or leaves the file unchanged. + * + * @param fileName The complete path of the file to be written + * @param data The data to be written to the file + * @return true if the data was written to the file, false if not. + */ + public static boolean writeFileAtomically(String fileName, byte[] data) { + return nativeWriteFileAtomically(fileName, data); + } + + private static native boolean nativeWriteFileAtomically( + String fileName, byte[] data); +} diff --git a/base/android/java/src/org/chromium/base/JNIUtils.java b/base/android/java/src/org/chromium/base/JNIUtils.java new file mode 100644 index 0000000000..3fcec91316 --- /dev/null +++ b/base/android/java/src/org/chromium/base/JNIUtils.java @@ -0,0 +1,46 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.MainDex; + +/** + * This class provides JNI-related methods to the native library. + */ +@MainDex +public class JNIUtils { + private static Boolean sSelectiveJniRegistrationEnabled; + + /** + * This returns a ClassLoader that is capable of loading Chromium Java code. Such a ClassLoader + * is needed for the few cases where the JNI mechanism is unable to automatically determine the + * appropriate ClassLoader instance. + */ + @CalledByNative + public static Object getClassLoader() { + return JNIUtils.class.getClassLoader(); + } + + /** + * @return whether or not the current process supports selective JNI registration. + */ + @CalledByNative + public static boolean isSelectiveJniRegistrationEnabled() { + if (sSelectiveJniRegistrationEnabled == null) { + sSelectiveJniRegistrationEnabled = false; + } + return sSelectiveJniRegistrationEnabled; + } + + /** + * Allow this process to selectively perform JNI registration. This must be called before + * loading native libraries or it will have no effect. + */ + public static void enableSelectiveJniRegistration() { + assert sSelectiveJniRegistrationEnabled == null; + sSelectiveJniRegistrationEnabled = true; + } +} diff --git a/base/android/java/src/org/chromium/base/JavaHandlerThread.java b/base/android/java/src/org/chromium/base/JavaHandlerThread.java new file mode 100644 index 0000000000..9a1c924398 --- /dev/null +++ b/base/android/java/src/org/chromium/base/JavaHandlerThread.java @@ -0,0 +1,119 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.MainDex; + +import java.lang.Thread.UncaughtExceptionHandler; + +/** + * Thread in Java with an Android Handler. This class is not thread safe. + */ +@JNINamespace("base::android") +@MainDex +public class JavaHandlerThread { + private final HandlerThread mThread; + + private Throwable mUnhandledException; + + /** + * Construct a java-only instance. Can be connected with native side later. + * Useful for cases where a java thread is needed before native library is loaded. + */ + public JavaHandlerThread(String name, int priority) { + mThread = new HandlerThread(name, priority); + } + + @CalledByNative + private static JavaHandlerThread create(String name, int priority) { + return new JavaHandlerThread(name, priority); + } + + public Looper getLooper() { + assert hasStarted(); + return mThread.getLooper(); + } + + public void maybeStart() { + if (hasStarted()) return; + mThread.start(); + } + + @CalledByNative + private void startAndInitialize(final long nativeThread, final long nativeEvent) { + maybeStart(); + new Handler(mThread.getLooper()).post(new Runnable() { + @Override + public void run() { + nativeInitializeThread(nativeThread, nativeEvent); + } + }); + } + + @CalledByNative + private void quitThreadSafely(final long nativeThread) { + // Allow pending java tasks to run, but don't run any delayed or newly queued up tasks. + new Handler(mThread.getLooper()).post(new Runnable() { + @Override + public void run() { + mThread.quit(); + nativeOnLooperStopped(nativeThread); + } + }); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + // When we can, signal that new tasks queued up won't be run. + mThread.getLooper().quitSafely(); + } + } + + @CalledByNative + private void joinThread() { + boolean joined = false; + while (!joined) { + try { + mThread.join(); + joined = true; + } catch (InterruptedException e) { + } + } + } + + private boolean hasStarted() { + return mThread.getState() != Thread.State.NEW; + } + + @CalledByNative + private boolean isAlive() { + return mThread.isAlive(); + } + + // This should *only* be used for tests. In production we always need to call the original + // uncaught exception handler (the framework's) after any uncaught exception handling we do, as + // it generates crash dumps and kills the process. + @CalledByNative + private void listenForUncaughtExceptionsForTesting() { + mThread.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + mUnhandledException = e; + } + }); + } + + @CalledByNative + private Throwable getUncaughtExceptionIfAny() { + return mUnhandledException; + } + + private native void nativeInitializeThread(long nativeJavaHandlerThread, long nativeEvent); + private native void nativeOnLooperStopped(long nativeJavaHandlerThread); +} diff --git a/base/android/java/src/org/chromium/base/LocaleUtils.java b/base/android/java/src/org/chromium/base/LocaleUtils.java new file mode 100644 index 0000000000..05d39029a5 --- /dev/null +++ b/base/android/java/src/org/chromium/base/LocaleUtils.java @@ -0,0 +1,207 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.LocaleList; +import android.text.TextUtils; + +import org.chromium.base.annotations.CalledByNative; + +import java.util.ArrayList; +import java.util.Locale; + +/** + * This class provides the locale related methods. + */ +public class LocaleUtils { + /** + * Guards this class from being instantiated. + */ + private LocaleUtils() { + } + + /** + * Java keeps deprecated language codes for Hebrew, Yiddish and Indonesian but Chromium uses + * updated ones. Similarly, Android uses "tl" while Chromium uses "fil" for Tagalog/Filipino. + * So apply a mapping here. + * See http://developer.android.com/reference/java/util/Locale.html + * @return a updated language code for Chromium with given language string. + */ + public static String getUpdatedLanguageForChromium(String language) { + // IMPORTANT: Keep in sync with the mapping found in: + // build/android/gyp/util/resource_utils.py + switch (language) { + case "iw": + return "he"; // Hebrew + case "ji": + return "yi"; // Yiddish + case "in": + return "id"; // Indonesian + case "tl": + return "fil"; // Filipino + default: + return language; + } + } + + /** + * @return a locale with updated language codes for Chromium, with translated modern language + * codes used by Chromium. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @VisibleForTesting + public static Locale getUpdatedLocaleForChromium(Locale locale) { + String language = locale.getLanguage(); + String languageForChrome = getUpdatedLanguageForChromium(language); + if (languageForChrome.equals(language)) { + return locale; + } + return new Locale.Builder().setLocale(locale).setLanguage(languageForChrome).build(); + } + + /** + * Android uses "tl" while Chromium uses "fil" for Tagalog/Filipino. + * So apply a mapping here. + * See http://developer.android.com/reference/java/util/Locale.html + * @return a updated language code for Android with given language string. + */ + public static String getUpdatedLanguageForAndroid(String language) { + // IMPORTANT: Keep in sync with the mapping found in: + // build/android/gyp/util/resource_utils.py + switch (language) { + case "und": + return ""; // Undefined + case "fil": + return "tl"; // Filipino + default: + return language; + } + } + + /** + * @return a locale with updated language codes for Android, from translated modern language + * codes used by Chromium. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @VisibleForTesting + public static Locale getUpdatedLocaleForAndroid(Locale locale) { + String language = locale.getLanguage(); + String languageForAndroid = getUpdatedLanguageForAndroid(language); + if (languageForAndroid.equals(language)) { + return locale; + } + return new Locale.Builder().setLocale(locale).setLanguage(languageForAndroid).build(); + } + + /** + * This function creates a Locale object from xx-XX style string where xx is language code + * and XX is a country code. This works for API level lower than 21. + * @return the locale that best represents the language tag. + */ + public static Locale forLanguageTagCompat(String languageTag) { + String[] tag = languageTag.split("-"); + if (tag.length == 0) { + return new Locale(""); + } + String language = getUpdatedLanguageForAndroid(tag[0]); + if ((language.length() != 2 && language.length() != 3)) { + return new Locale(""); + } + if (tag.length == 1) { + return new Locale(language); + } + String country = tag[1]; + if (country.length() != 2 && country.length() != 3) { + return new Locale(language); + } + return new Locale(language, country); + } + + /** + * This function creates a Locale object from xx-XX style string where xx is language code + * and XX is a country code. + * @return the locale that best represents the language tag. + */ + public static Locale forLanguageTag(String languageTag) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Locale locale = Locale.forLanguageTag(languageTag); + return getUpdatedLocaleForAndroid(locale); + } + return forLanguageTagCompat(languageTag); + } + + /** + * Converts Locale object to the BCP 47 compliant string format. + * This works for API level lower than 24. + * + * Note that for Android M or before, we cannot use Locale.getLanguage() and + * Locale.toLanguageTag() for this purpose. Since Locale.getLanguage() returns deprecated + * language code even if the Locale object is constructed with updated language code. As for + * Locale.toLanguageTag(), it does a special conversion from deprecated language code to updated + * one, but it is only usable for Android N or after. + * @return a well-formed IETF BCP 47 language tag with language and country code that + * represents this locale. + */ + public static String toLanguageTag(Locale locale) { + String language = getUpdatedLanguageForChromium(locale.getLanguage()); + String country = locale.getCountry(); + if (language.equals("no") && country.equals("NO") && locale.getVariant().equals("NY")) { + return "nn-NO"; + } + return country.isEmpty() ? language : language + "-" + country; + } + + /** + * Converts LocaleList object to the comma separated BCP 47 compliant string format. + * + * @return a well-formed IETF BCP 47 language tag with language and country code that + * represents this locale list. + */ + @TargetApi(Build.VERSION_CODES.N) + public static String toLanguageTags(LocaleList localeList) { + ArrayList<String> newLocaleList = new ArrayList<>(); + for (int i = 0; i < localeList.size(); i++) { + Locale locale = getUpdatedLocaleForChromium(localeList.get(i)); + newLocaleList.add(toLanguageTag(locale)); + } + return TextUtils.join(",", newLocaleList); + } + + /** + * @return a comma separated language tags string that represents a default locale. + * Each language tag is well-formed IETF BCP 47 language tag with language and country + * code. + */ + @CalledByNative + public static String getDefaultLocaleString() { + return toLanguageTag(Locale.getDefault()); + } + + /** + * @return a comma separated language tags string that represents a default locale or locales. + * Each language tag is well-formed IETF BCP 47 language tag with language and country + * code. + */ + public static String getDefaultLocaleListString() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return toLanguageTags(LocaleList.getDefault()); + } + return getDefaultLocaleString(); + } + + /** + * @return The default country code set during install. + */ + @CalledByNative + private static String getDefaultCountryCode() { + CommandLine commandLine = CommandLine.getInstance(); + return commandLine.hasSwitch(BaseSwitches.DEFAULT_COUNTRY_CODE_AT_INSTALL) + ? commandLine.getSwitchValue(BaseSwitches.DEFAULT_COUNTRY_CODE_AT_INSTALL) + : Locale.getDefault().getCountry(); + } + +} diff --git a/base/android/java/src/org/chromium/base/MemoryPressureListener.java b/base/android/java/src/org/chromium/base/MemoryPressureListener.java new file mode 100644 index 0000000000..6c80970f48 --- /dev/null +++ b/base/android/java/src/org/chromium/base/MemoryPressureListener.java @@ -0,0 +1,130 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.app.Activity; +import android.content.ComponentCallbacks2; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.MainDex; +import org.chromium.base.memory.MemoryPressureCallback; + +/** + * This class is Java equivalent of base::MemoryPressureListener: it distributes pressure + * signals to callbacks. + * + * The class also serves as an entry point to the native side - once native code is ready, + * it adds native callback. + * + * notifyMemoryPressure() is called exclusively by MemoryPressureMonitor, which + * monitors and throttles pressure signals. + * + * NOTE: this class should only be used on UiThread as defined by ThreadUtils (which is + * Android main thread for Chrome, but can be some other thread for WebView). + */ +@MainDex +public class MemoryPressureListener { + /** + * Sending an intent with this action to Chrome will cause it to issue a call to onLowMemory + * thus simulating a low memory situations. + */ + private static final String ACTION_LOW_MEMORY = "org.chromium.base.ACTION_LOW_MEMORY"; + + /** + * Sending an intent with this action to Chrome will cause it to issue a call to onTrimMemory + * thus simulating a low memory situations. + */ + private static final String ACTION_TRIM_MEMORY = "org.chromium.base.ACTION_TRIM_MEMORY"; + + /** + * Sending an intent with this action to Chrome will cause it to issue a call to onTrimMemory + * with notification level TRIM_MEMORY_RUNNING_CRITICAL thus simulating a low memory situation + */ + private static final String ACTION_TRIM_MEMORY_RUNNING_CRITICAL = + "org.chromium.base.ACTION_TRIM_MEMORY_RUNNING_CRITICAL"; + + /** + * Sending an intent with this action to Chrome will cause it to issue a call to onTrimMemory + * with notification level TRIM_MEMORY_MODERATE thus simulating a low memory situation + */ + private static final String ACTION_TRIM_MEMORY_MODERATE = + "org.chromium.base.ACTION_TRIM_MEMORY_MODERATE"; + + private static final ObserverList<MemoryPressureCallback> sCallbacks = new ObserverList<>(); + + /** + * Called by the native side to add native callback. + */ + @CalledByNative + private static void addNativeCallback() { + addCallback(MemoryPressureListener::nativeOnMemoryPressure); + } + + /** + * Adds a memory pressure callback. + * Callback is only added once, regardless of the number of addCallback() calls. + * This method should be called only on ThreadUtils.UiThread. + */ + public static void addCallback(MemoryPressureCallback callback) { + sCallbacks.addObserver(callback); + } + + /** + * Removes previously added memory pressure callback. + * This method should be called only on ThreadUtils.UiThread. + */ + public static void removeCallback(MemoryPressureCallback callback) { + sCallbacks.removeObserver(callback); + } + + /** + * Distributes |pressure| to all callbacks. + * This method should be called only on ThreadUtils.UiThread. + */ + public static void notifyMemoryPressure(@MemoryPressureLevel int pressure) { + for (MemoryPressureCallback callback : sCallbacks) { + callback.onPressure(pressure); + } + } + + /** + * Used by applications to simulate a memory pressure signal. By throwing certain intent + * actions. + */ + public static boolean handleDebugIntent(Activity activity, String action) { + if (ACTION_LOW_MEMORY.equals(action)) { + simulateLowMemoryPressureSignal(activity); + } else if (ACTION_TRIM_MEMORY.equals(action)) { + simulateTrimMemoryPressureSignal(activity, ComponentCallbacks2.TRIM_MEMORY_COMPLETE); + } else if (ACTION_TRIM_MEMORY_RUNNING_CRITICAL.equals(action)) { + simulateTrimMemoryPressureSignal(activity, + ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL); + } else if (ACTION_TRIM_MEMORY_MODERATE.equals(action)) { + simulateTrimMemoryPressureSignal(activity, ComponentCallbacks2.TRIM_MEMORY_MODERATE); + } else { + return false; + } + + return true; + } + + private static void simulateLowMemoryPressureSignal(Activity activity) { + // The Application and the Activity each have a list of callbacks they notify when this + // method is called. Notifying these will simulate the event at the App/Activity level + // as well as trigger the listener bound from native in this process. + activity.getApplication().onLowMemory(); + activity.onLowMemory(); + } + + private static void simulateTrimMemoryPressureSignal(Activity activity, int level) { + // The Application and the Activity each have a list of callbacks they notify when this + // method is called. Notifying these will simulate the event at the App/Activity level + // as well as trigger the listener bound from native in this process. + activity.getApplication().onTrimMemory(level); + activity.onTrimMemory(level); + } + + private static native void nativeOnMemoryPressure(@MemoryPressureLevel int pressure); +} diff --git a/base/android/java/src/org/chromium/base/NonThreadSafe.java b/base/android/java/src/org/chromium/base/NonThreadSafe.java new file mode 100644 index 0000000000..53f38d2c81 --- /dev/null +++ b/base/android/java/src/org/chromium/base/NonThreadSafe.java @@ -0,0 +1,41 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +/** + * NonThreadSafe is a helper class used to help verify that methods of a + * class are called from the same thread. + */ +public class NonThreadSafe { + private Long mThreadId; + + public NonThreadSafe() { + ensureThreadIdAssigned(); + } + + /** + * Changes the thread that is checked for in CalledOnValidThread. This may + * be useful when an object may be created on one thread and then used + * exclusively on another thread. + */ + @VisibleForTesting + public synchronized void detachFromThread() { + mThreadId = null; + } + + /** + * Checks if the method is called on the valid thread. + * Assigns the current thread if no thread was assigned. + */ + @SuppressWarnings("NoSynchronizedMethodCheck") + public synchronized boolean calledOnValidThread() { + ensureThreadIdAssigned(); + return mThreadId.equals(Thread.currentThread().getId()); + } + + private void ensureThreadIdAssigned() { + if (mThreadId == null) mThreadId = Thread.currentThread().getId(); + } +} diff --git a/base/android/java/src/org/chromium/base/ObserverList.java b/base/android/java/src/org/chromium/base/ObserverList.java new file mode 100644 index 0000000000..59276c6ea8 --- /dev/null +++ b/base/android/java/src/org/chromium/base/ObserverList.java @@ -0,0 +1,249 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.annotation.concurrent.NotThreadSafe; + +/** + * A container for a list of observers. + * <p/> + * This container can be modified during iteration without invalidating the iterator. + * So, it safely handles the case of an observer removing itself or other observers from the list + * while observers are being notified. + * <p/> + * The implementation (and the interface) is heavily influenced by the C++ ObserverList. + * Notable differences: + * - The iterator implements NOTIFY_EXISTING_ONLY. + * - The range-based for loop is left to the clients to implement in terms of iterator(). + * <p/> + * This class is not threadsafe. Observers MUST be added, removed and will be notified on the same + * thread this is created. + * + * @param <E> The type of observers that this list should hold. + */ +@NotThreadSafe +public class ObserverList<E> implements Iterable<E> { + /** + * Extended iterator interface that provides rewind functionality. + */ + public interface RewindableIterator<E> extends Iterator<E> { + /** + * Rewind the iterator back to the beginning. + * + * If we need to iterate multiple times, we can avoid iterator object reallocation by using + * this method. + */ + public void rewind(); + } + + public final List<E> mObservers = new ArrayList<E>(); + private int mIterationDepth; + private int mCount; + private boolean mNeedsCompact; + + public ObserverList() {} + + /** + * Add an observer to the list. + * <p/> + * An observer should not be added to the same list more than once. If an iteration is already + * in progress, this observer will be not be visible during that iteration. + * + * @return true if the observer list changed as a result of the call. + */ + public boolean addObserver(E obs) { + // Avoid adding null elements to the list as they may be removed on a compaction. + if (obs == null || mObservers.contains(obs)) { + return false; + } + + // Structurally modifying the underlying list here. This means we + // cannot use the underlying list's iterator to iterate over the list. + boolean result = mObservers.add(obs); + assert result; + + ++mCount; + return true; + } + + /** + * Remove an observer from the list if it is in the list. + * + * @return true if an element was removed as a result of this call. + */ + public boolean removeObserver(E obs) { + if (obs == null) { + return false; + } + + int index = mObservers.indexOf(obs); + if (index == -1) { + return false; + } + + if (mIterationDepth == 0) { + // No one is iterating over the list. + mObservers.remove(index); + } else { + mNeedsCompact = true; + mObservers.set(index, null); + } + --mCount; + assert mCount >= 0; + + return true; + } + + public boolean hasObserver(E obs) { + return mObservers.contains(obs); + } + + public void clear() { + mCount = 0; + + if (mIterationDepth == 0) { + mObservers.clear(); + return; + } + + int size = mObservers.size(); + mNeedsCompact |= size != 0; + for (int i = 0; i < size; i++) { + mObservers.set(i, null); + } + } + + @Override + public Iterator<E> iterator() { + return new ObserverListIterator(); + } + + /** + * It's the same as {@link ObserverList#iterator()} but the return type is + * {@link RewindableIterator}. Use this iterator type if you need to use + * {@link RewindableIterator#rewind()}. + */ + public RewindableIterator<E> rewindableIterator() { + return new ObserverListIterator(); + } + + /** + * Returns the number of observers currently registered in the ObserverList. + * This is equivalent to the number of non-empty spaces in |mObservers|. + */ + public int size() { + return mCount; + } + + /** + * Returns true if the ObserverList contains no observers. + */ + public boolean isEmpty() { + return mCount == 0; + } + + /** + * Compact the underlying list be removing null elements. + * <p/> + * Should only be called when mIterationDepth is zero. + */ + private void compact() { + assert mIterationDepth == 0; + for (int i = mObservers.size() - 1; i >= 0; i--) { + if (mObservers.get(i) == null) { + mObservers.remove(i); + } + } + } + + private void incrementIterationDepth() { + mIterationDepth++; + } + + private void decrementIterationDepthAndCompactIfNeeded() { + mIterationDepth--; + assert mIterationDepth >= 0; + if (mIterationDepth > 0) return; + if (!mNeedsCompact) return; + mNeedsCompact = false; + compact(); + } + + /** + * Returns the size of the underlying storage of the ObserverList. + * It will take into account the empty spaces inside |mObservers|. + */ + private int capacity() { + return mObservers.size(); + } + + private E getObserverAt(int index) { + return mObservers.get(index); + } + + private class ObserverListIterator implements RewindableIterator<E> { + private int mListEndMarker; + private int mIndex; + private boolean mIsExhausted; + + private ObserverListIterator() { + ObserverList.this.incrementIterationDepth(); + mListEndMarker = ObserverList.this.capacity(); + } + + @Override + public void rewind() { + compactListIfNeeded(); + ObserverList.this.incrementIterationDepth(); + mListEndMarker = ObserverList.this.capacity(); + mIsExhausted = false; + mIndex = 0; + } + + @Override + public boolean hasNext() { + int lookupIndex = mIndex; + while (lookupIndex < mListEndMarker + && ObserverList.this.getObserverAt(lookupIndex) == null) { + lookupIndex++; + } + if (lookupIndex < mListEndMarker) return true; + + // We have reached the end of the list, allow for compaction. + compactListIfNeeded(); + return false; + } + + @Override + public E next() { + // Advance if the current element is null. + while (mIndex < mListEndMarker && ObserverList.this.getObserverAt(mIndex) == null) { + mIndex++; + } + if (mIndex < mListEndMarker) return ObserverList.this.getObserverAt(mIndex++); + + // We have reached the end of the list, allow for compaction. + compactListIfNeeded(); + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private void compactListIfNeeded() { + if (!mIsExhausted) { + mIsExhausted = true; + ObserverList.this.decrementIterationDepthAndCompactIfNeeded(); + } + } + } +} diff --git a/base/android/java/src/org/chromium/base/PathService.java b/base/android/java/src/org/chromium/base/PathService.java new file mode 100644 index 0000000000..9807c2e82a --- /dev/null +++ b/base/android/java/src/org/chromium/base/PathService.java @@ -0,0 +1,26 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import org.chromium.base.annotations.JNINamespace; + +/** + * This class provides java side access to the native PathService. + */ +@JNINamespace("base::android") +public abstract class PathService { + + // Must match the value of DIR_MODULE in base/base_paths.h! + public static final int DIR_MODULE = 3; + + // Prevent instantiation. + private PathService() {} + + public static void override(int what, String path) { + nativeOverride(what, path); + } + + private static native void nativeOverride(int what, String path); +} diff --git a/base/android/java/src/org/chromium/base/PathUtils.java b/base/android/java/src/org/chromium/base/PathUtils.java new file mode 100644 index 0000000000..e6fc8029b8 --- /dev/null +++ b/base/android/java/src/org/chromium/base/PathUtils.java @@ -0,0 +1,263 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.os.Build; +import android.os.Environment; +import android.os.SystemClock; +import android.system.Os; +import android.text.TextUtils; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.MainDex; +import org.chromium.base.metrics.RecordHistogram; + +import java.io.File; +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This class provides the path related methods for the native library. + */ +@MainDex +public abstract class PathUtils { + private static final String TAG = "PathUtils"; + private static final String THUMBNAIL_DIRECTORY_NAME = "textures"; + + private static final int DATA_DIRECTORY = 0; + private static final int THUMBNAIL_DIRECTORY = 1; + private static final int CACHE_DIRECTORY = 2; + private static final int NUM_DIRECTORIES = 3; + private static final AtomicBoolean sInitializationStarted = new AtomicBoolean(); + private static AsyncTask<Void, Void, String[]> sDirPathFetchTask; + + // If the AsyncTask started in setPrivateDataDirectorySuffix() fails to complete by the time we + // need the values, we will need the suffix so that we can restart the task synchronously on + // the UI thread. + private static String sDataDirectorySuffix; + private static String sCacheSubDirectory; + + // Prevent instantiation. + private PathUtils() {} + + /** + * Initialization-on-demand holder. This exists for thread-safe lazy initialization. It will + * cause getOrComputeDirectoryPaths() to be called (safely) the first time DIRECTORY_PATHS is + * accessed. + * + * <p>See https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom. + */ + private static class Holder { + private static final String[] DIRECTORY_PATHS = getOrComputeDirectoryPaths(); + } + + /** + * Get the directory paths from sDirPathFetchTask if available, or compute it synchronously + * on the UI thread otherwise. This should only be called as part of Holder's initialization + * above to guarantee thread-safety as part of the initialization-on-demand holder idiom. + */ + private static String[] getOrComputeDirectoryPaths() { + try { + // We need to call sDirPathFetchTask.cancel() here to prevent races. If it returns + // true, that means that the task got canceled successfully (and thus, it did not + // finish running its task). Otherwise, it failed to cancel, meaning that it was + // already finished. + if (sDirPathFetchTask.cancel(false)) { + // Allow disk access here because we have no other choice. + try (StrictModeContext unused = StrictModeContext.allowDiskWrites()) { + // sDirPathFetchTask did not complete. We have to run the code it was supposed + // to be responsible for synchronously on the UI thread. + return PathUtils.setPrivateDataDirectorySuffixInternal(); + } + } else { + // sDirPathFetchTask succeeded, and the values we need should be ready to access + // synchronously in its internal future. + return sDirPathFetchTask.get(); + } + } catch (InterruptedException e) { + } catch (ExecutionException e) { + } + + return null; + } + + @SuppressLint("NewApi") + private static void chmod(String path, int mode) { + // Both Os.chmod and ErrnoException require SDK >= 21. But while Dalvik on < 21 tolerates + // Os.chmod, it throws VerifyError for ErrnoException, so catch Exception instead. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return; + try { + Os.chmod(path, mode); + } catch (Exception e) { + Log.e(TAG, "Failed to set permissions for path \"" + path + "\""); + } + } + + /** + * Fetch the path of the directory where private data is to be stored by the application. This + * is meant to be called in an AsyncTask in setPrivateDataDirectorySuffix(), but if we need the + * result before the AsyncTask has had a chance to finish, then it's best to cancel the task + * and run it on the UI thread instead, inside getOrComputeDirectoryPaths(). + * + * @see Context#getDir(String, int) + */ + private static String[] setPrivateDataDirectorySuffixInternal() { + String[] paths = new String[NUM_DIRECTORIES]; + Context appContext = ContextUtils.getApplicationContext(); + paths[DATA_DIRECTORY] = appContext.getDir( + sDataDirectorySuffix, Context.MODE_PRIVATE).getPath(); + // MODE_PRIVATE results in rwxrwx--x, but we want rwx------, as a defence-in-depth measure. + chmod(paths[DATA_DIRECTORY], 0700); + paths[THUMBNAIL_DIRECTORY] = appContext.getDir( + THUMBNAIL_DIRECTORY_NAME, Context.MODE_PRIVATE).getPath(); + if (appContext.getCacheDir() != null) { + if (sCacheSubDirectory == null) { + paths[CACHE_DIRECTORY] = appContext.getCacheDir().getPath(); + } else { + paths[CACHE_DIRECTORY] = + new File(appContext.getCacheDir(), sCacheSubDirectory).getPath(); + } + } + return paths; + } + + /** + * Starts an asynchronous task to fetch the path of the directory where private data is to be + * stored by the application. + * + * <p>This task can run long (or more likely be delayed in a large task queue), in which case we + * want to cancel it and run on the UI thread instead. Unfortunately, this means keeping a bit + * of extra static state - we need to store the suffix and the application context in case we + * need to try to re-execute later. + * + * @param suffix The private data directory suffix. + * @param cacheSubDir The subdirectory in the cache directory to use, if non-null. + * @see Context#getDir(String, int) + */ + public static void setPrivateDataDirectorySuffix(String suffix, String cacheSubDir) { + // This method should only be called once, but many tests end up calling it multiple times, + // so adding a guard here. + if (!sInitializationStarted.getAndSet(true)) { + assert ContextUtils.getApplicationContext() != null; + sDataDirectorySuffix = suffix; + sCacheSubDirectory = cacheSubDir; + sDirPathFetchTask = new AsyncTask<Void, Void, String[]>() { + @Override + protected String[] doInBackground(Void... unused) { + return PathUtils.setPrivateDataDirectorySuffixInternal(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } + + public static void setPrivateDataDirectorySuffix(String suffix) { + setPrivateDataDirectorySuffix(suffix, null); + } + + /** + * @param index The index of the cached directory path. + * @return The directory path requested. + */ + private static String getDirectoryPath(int index) { + return Holder.DIRECTORY_PATHS[index]; + } + + /** + * @return the private directory that is used to store application data. + */ + @CalledByNative + public static String getDataDirectory() { + assert sDirPathFetchTask != null : "setDataDirectorySuffix must be called first."; + return getDirectoryPath(DATA_DIRECTORY); + } + + /** + * @return the cache directory. + */ + @CalledByNative + public static String getCacheDirectory() { + assert sDirPathFetchTask != null : "setDataDirectorySuffix must be called first."; + return getDirectoryPath(CACHE_DIRECTORY); + } + + @CalledByNative + public static String getThumbnailCacheDirectory() { + assert sDirPathFetchTask != null : "setDataDirectorySuffix must be called first."; + return getDirectoryPath(THUMBNAIL_DIRECTORY); + } + + /** + * @return the public downloads directory. + */ + @SuppressWarnings("unused") + @CalledByNative + private static String getDownloadsDirectory() { + // Temporarily allowing disk access while fixing. TODO: http://crbug.com/508615 + try (StrictModeContext unused = StrictModeContext.allowDiskReads()) { + long time = SystemClock.elapsedRealtime(); + String downloadsPath = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + .getPath(); + RecordHistogram.recordTimesHistogram("Android.StrictMode.DownloadsDir", + SystemClock.elapsedRealtime() - time, TimeUnit.MILLISECONDS); + return downloadsPath; + } + } + + /** + * @return Download directories including the default storage directory on SD card, and a + * private directory on external SD card. + */ + @SuppressWarnings("unused") + @CalledByNative + public static String[] getAllPrivateDownloadsDirectories() { + File[] files; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + try (StrictModeContext unused = StrictModeContext.allowDiskWrites()) { + files = ContextUtils.getApplicationContext().getExternalFilesDirs( + Environment.DIRECTORY_DOWNLOADS); + } + } else { + files = new File[] {Environment.getExternalStorageDirectory()}; + } + + ArrayList<String> absolutePaths = new ArrayList<String>(); + for (int i = 0; i < files.length; ++i) { + if (files[i] == null || TextUtils.isEmpty(files[i].getAbsolutePath())) continue; + absolutePaths.add(files[i].getAbsolutePath()); + } + + return absolutePaths.toArray(new String[absolutePaths.size()]); + } + + /** + * @return the path to native libraries. + */ + @SuppressWarnings("unused") + @CalledByNative + private static String getNativeLibraryDirectory() { + ApplicationInfo ai = ContextUtils.getApplicationContext().getApplicationInfo(); + if ((ai.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 + || (ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + return ai.nativeLibraryDir; + } + + return "/system/lib/"; + } + + /** + * @return the external storage directory. + */ + @SuppressWarnings("unused") + @CalledByNative + public static String getExternalStorageDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath(); + } +} diff --git a/base/android/java/src/org/chromium/base/PowerMonitor.java b/base/android/java/src/org/chromium/base/PowerMonitor.java new file mode 100644 index 0000000000..ae36a75d00 --- /dev/null +++ b/base/android/java/src/org/chromium/base/PowerMonitor.java @@ -0,0 +1,80 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; + +/** + * Integrates native PowerMonitor with the java side. + */ +@JNINamespace("base::android") +public class PowerMonitor { + private static PowerMonitor sInstance; + + private boolean mIsBatteryPower; + + public static void createForTests() { + // Applications will create this once the JNI side has been fully wired up both sides. For + // tests, we just need native -> java, that is, we don't need to notify java -> native on + // creation. + sInstance = new PowerMonitor(); + } + + /** + * Create a PowerMonitor instance if none exists. + */ + public static void create() { + ThreadUtils.assertOnUiThread(); + + if (sInstance != null) return; + + Context context = ContextUtils.getApplicationContext(); + sInstance = new PowerMonitor(); + IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + Intent batteryStatusIntent = context.registerReceiver(null, ifilter); + if (batteryStatusIntent != null) onBatteryChargingChanged(batteryStatusIntent); + + IntentFilter powerConnectedFilter = new IntentFilter(); + powerConnectedFilter.addAction(Intent.ACTION_POWER_CONNECTED); + powerConnectedFilter.addAction(Intent.ACTION_POWER_DISCONNECTED); + context.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + PowerMonitor.onBatteryChargingChanged(intent); + } + }, powerConnectedFilter); + } + + private PowerMonitor() { + } + + private static void onBatteryChargingChanged(Intent intent) { + assert sInstance != null; + int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + // If we're not plugged, assume we're running on battery power. + sInstance.mIsBatteryPower = chargePlug != BatteryManager.BATTERY_PLUGGED_USB + && chargePlug != BatteryManager.BATTERY_PLUGGED_AC; + nativeOnBatteryChargingChanged(); + } + + @CalledByNative + private static boolean isBatteryPower() { + // Creation of the PowerMonitor can be deferred based on the browser startup path. If the + // battery power is requested prior to the browser triggering the creation, force it to be + // created now. + if (sInstance == null) create(); + + return sInstance.mIsBatteryPower; + } + + private static native void nativeOnBatteryChargingChanged(); +} diff --git a/base/android/java/src/org/chromium/base/Promise.java b/base/android/java/src/org/chromium/base/Promise.java new file mode 100644 index 0000000000..4319148d9c --- /dev/null +++ b/base/android/java/src/org/chromium/base/Promise.java @@ -0,0 +1,294 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.os.Handler; +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.LinkedList; +import java.util.List; + +/** + * A Promise class to be used as a placeholder for a result that will be provided asynchronously. + * It must only be accessed from a single thread. + * @param <T> The type the Promise will be fulfilled with. + */ +public class Promise<T> { + // TODO(peconn): Implement rejection handlers that can recover from rejection. + + @Retention(RetentionPolicy.SOURCE) + @IntDef({UNFULFILLED, FULFILLED, REJECTED}) + private @interface PromiseState {} + + private static final int UNFULFILLED = 0; + private static final int FULFILLED = 1; + private static final int REJECTED = 2; + + @PromiseState + private int mState = UNFULFILLED; + + private T mResult; + private final List<Callback<T>> mFulfillCallbacks = new LinkedList<>(); + + private Exception mRejectReason; + private final List<Callback<Exception>> mRejectCallbacks = new LinkedList<>(); + + private final Thread mThread = Thread.currentThread(); + private final Handler mHandler = new Handler(); + + private boolean mThrowingRejectionHandler; + + /** + * A function class for use when chaining Promises with {@link Promise#then(Function)}. + * @param <A> The type of the function input. + * @param <R> The type of the function output. + */ + public interface Function<A, R> { + R apply(A argument); + } + + /** + * A function class for use when chaining Promises with {@link Promise#then(AsyncFunction)}. + * @param <A> The type of the function input. + * @param <R> The type of the function output. + */ + public interface AsyncFunction<A, R> { + Promise<R> apply(A argument); + } + + /** + * An exception class for when a rejected Promise is not handled and cannot pass the rejection + * to a subsequent Promise. + */ + public static class UnhandledRejectionException extends RuntimeException { + public UnhandledRejectionException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Convenience method that calls {@link #then(Callback, Callback)} providing a rejection + * {@link Callback} that throws a {@link UnhandledRejectionException}. Only use this on + * Promises that do not have rejection handlers or dependant Promises. + */ + public void then(Callback<T> onFulfill) { + checkThread(); + + // Allow multiple single argument then(Callback)'s, but don't bother adding duplicate + // throwing rejection handlers. + if (mThrowingRejectionHandler) { + thenInner(onFulfill); + return; + } + + assert mRejectCallbacks.size() == 0 : "Do not call the single argument " + + "Promise.then(Callback) on a Promise that already has a rejection handler."; + + Callback<Exception> onReject = reason -> { + throw new UnhandledRejectionException( + "Promise was rejected without a rejection handler.", reason); + }; + + then(onFulfill, onReject); + mThrowingRejectionHandler = true; + } + + /** + * Queues {@link Callback}s to be run when the Promise is either fulfilled or rejected. If the + * Promise is already fulfilled or rejected, the appropriate callback will be run on the next + * iteration of the message loop. + * + * @param onFulfill The Callback to be called on fulfillment. + * @param onReject The Callback to be called on rejection. The argument to onReject will + * may be null if the Promise was rejected manually. + */ + public void then(Callback<T> onFulfill, Callback<Exception> onReject) { + checkThread(); + thenInner(onFulfill); + exceptInner(onReject); + } + + /** + * Adds a rejection handler to the Promise. This handler will be called if this Promise or any + * Promises this Promise depends on is rejected or fails. The {@link Callback} will be given + * the exception that caused the rejection, or null if the rejection was manual (caused by a + * call to {@link #reject()}. + */ + public void except(Callback<Exception> onReject) { + checkThread(); + exceptInner(onReject); + } + + private void thenInner(Callback<T> onFulfill) { + if (mState == FULFILLED) { + postCallbackToLooper(onFulfill, mResult); + } else if (mState == UNFULFILLED) { + mFulfillCallbacks.add(onFulfill); + } + } + + private void exceptInner(Callback<Exception> onReject) { + assert !mThrowingRejectionHandler : "Do not add an exception handler to a Promise you have " + + "called the single argument Promise.then(Callback) on."; + + if (mState == REJECTED) { + postCallbackToLooper(onReject, mRejectReason); + } else if (mState == UNFULFILLED) { + mRejectCallbacks.add(onReject); + } + } + + /** + * Queues a {@link Promise.Function} to be run when the Promise is fulfilled. When this Promise + * is fulfilled, the function will be run and its result will be place in the returned Promise. + */ + public <R> Promise<R> then(final Function<T, R> function) { + checkThread(); + + // Create a new Promise to store the result of the function. + final Promise<R> promise = new Promise<>(); + + // Once this Promise is fulfilled: + // - Apply the given function to the result. + // - Fulfill the new Promise. + thenInner(result -> { + try { + promise.fulfill(function.apply(result)); + } catch (Exception e) { + // If function application fails, reject the next Promise. + promise.reject(e); + } + }); + + // If this Promise is rejected, reject the next Promise. + exceptInner(promise::reject); + + return promise; + } + + /** + * Queues a {@link Promise.AsyncFunction} to be run when the Promise is fulfilled. When this + * Promise is fulfilled, the AsyncFunction will be run. When the result of the AsyncFunction is + * available, it will be placed in the returned Promise. + */ + public <R> Promise<R> then(final AsyncFunction<T, R> function) { + checkThread(); + + // Create a new Promise to be returned. + final Promise<R> promise = new Promise<>(); + + // Once this Promise is fulfilled: + // - Apply the given function to the result (giving us an inner Promise). + // - On fulfillment of this inner Promise, fulfill our return Promise. + thenInner(result -> { + try { + // When the inner Promise is fulfilled, fulfill the return Promise. + // Alternatively, if the inner Promise is rejected, reject the return Promise. + function.apply(result).then(promise::fulfill, promise::reject); + } catch (Exception e) { + // If creating the inner Promise failed, reject the next Promise. + promise.reject(e); + } + }); + + // If this Promise is rejected, reject the next Promise. + exceptInner(promise::reject); + + return promise; + } + + /** + * Fulfills the Promise with the result and passes it to any {@link Callback}s previously queued + * on the next iteration of the message loop. + */ + public void fulfill(final T result) { + checkThread(); + assert mState == UNFULFILLED; + + mState = FULFILLED; + mResult = result; + + for (final Callback<T> callback : mFulfillCallbacks) { + postCallbackToLooper(callback, result); + } + + mFulfillCallbacks.clear(); + } + + /** + * Rejects the Promise, rejecting all those Promises that rely on it. + * + * This may throw an exception if a dependent Promise fails to handle the rejection, so it is + * important to make it explicit when a Promise may be rejected, so that users of that Promise + * know to provide rejection handling. + */ + public void reject(final Exception reason) { + checkThread(); + assert mState == UNFULFILLED; + + mState = REJECTED; + mRejectReason = reason; + + for (final Callback<Exception> callback : mRejectCallbacks) { + postCallbackToLooper(callback, reason); + } + mRejectCallbacks.clear(); + } + + /** + * Rejects a Promise, see {@link #reject(Exception)}. + */ + public void reject() { + reject(null); + } + + /** + * Returns whether the promise is fulfilled. + */ + public boolean isFulfilled() { + checkThread(); + return mState == FULFILLED; + } + + /** + * Returns whether the promise is rejected. + */ + public boolean isRejected() { + checkThread(); + return mState == REJECTED; + } + + /** + * Must be called after the promise has been fulfilled. + * + * @return The promised result. + */ + public T getResult() { + assert isFulfilled(); + return mResult; + } + + /** + * Convenience method to return a Promise fulfilled with the given result. + */ + public static <T> Promise<T> fulfilled(T result) { + Promise<T> promise = new Promise<>(); + promise.fulfill(result); + return promise; + } + + private void checkThread() { + assert mThread == Thread.currentThread() : "Promise must only be used on a single Thread."; + } + + // We use a different template parameter here so this can be used for both T and Throwables. + private <S> void postCallbackToLooper(final Callback<S> callback, final S result) { + // Post the callbacks to the Thread looper so we don't get a long chain of callbacks + // holding up the thread. + mHandler.post(() -> callback.onResult(result)); + } +} diff --git a/base/android/java/src/org/chromium/base/SecureRandomInitializer.java b/base/android/java/src/org/chromium/base/SecureRandomInitializer.java new file mode 100644 index 0000000000..bfd7b4943a --- /dev/null +++ b/base/android/java/src/org/chromium/base/SecureRandomInitializer.java @@ -0,0 +1,35 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.annotation.SuppressLint; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.SecureRandom; + +/** + * This class contains code to initialize a SecureRandom generator securely on Android platforms + * <= 4.3. See + * {@link http://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html}. + */ +// TODO(crbug.com/635567): Fix this properly. +@SuppressLint("SecureRandom") +public class SecureRandomInitializer { + private static final int NUM_RANDOM_BYTES = 16; + + /** + * Safely initializes the random number generator, by seeding it with data from /dev/urandom. + */ + public static void initialize(SecureRandom generator) throws IOException { + try (FileInputStream fis = new FileInputStream("/dev/urandom")) { + byte[] seedBytes = new byte[NUM_RANDOM_BYTES]; + if (fis.read(seedBytes) != seedBytes.length) { + throw new IOException("Failed to get enough random data."); + } + generator.setSeed(seedBytes); + } + } +} diff --git a/base/android/java/src/org/chromium/base/StreamUtil.java b/base/android/java/src/org/chromium/base/StreamUtil.java new file mode 100644 index 0000000000..f8cbfeeb9e --- /dev/null +++ b/base/android/java/src/org/chromium/base/StreamUtil.java @@ -0,0 +1,28 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Helper methods to deal with stream related tasks. + */ +public class StreamUtil { + /** + * Handle closing a {@link java.io.Closeable} via {@link java.io.Closeable#close()} and catch + * the potentially thrown {@link java.io.IOException}. + * @param closeable The Closeable to be closed. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable == null) return; + + try { + closeable.close(); + } catch (IOException ex) { + // Ignore the exception on close. + } + } +} diff --git a/base/android/java/src/org/chromium/base/SysUtils.java b/base/android/java/src/org/chromium/base/SysUtils.java new file mode 100644 index 0000000000..d4eb30de5b --- /dev/null +++ b/base/android/java/src/org/chromium/base/SysUtils.java @@ -0,0 +1,199 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.StrictMode; +import android.util.Log; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.metrics.CachedMetrics; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Exposes system related information about the current device. + */ +@JNINamespace("base::android") +public class SysUtils { + // A device reporting strictly more total memory in megabytes cannot be considered 'low-end'. + private static final int ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB = 512; + private static final int ANDROID_O_LOW_MEMORY_DEVICE_THRESHOLD_MB = 1024; + + private static final String TAG = "SysUtils"; + + private static Boolean sLowEndDevice; + private static Integer sAmountOfPhysicalMemoryKB; + + private static CachedMetrics.BooleanHistogramSample sLowEndMatches = + new CachedMetrics.BooleanHistogramSample("Android.SysUtilsLowEndMatches"); + + private SysUtils() { } + + /** + * Return the amount of physical memory on this device in kilobytes. + * @return Amount of physical memory in kilobytes, or 0 if there was + * an error trying to access the information. + */ + private static int detectAmountOfPhysicalMemoryKB() { + // Extract total memory RAM size by parsing /proc/meminfo, note that + // this is exactly what the implementation of sysconf(_SC_PHYS_PAGES) + // does. However, it can't be called because this method must be + // usable before any native code is loaded. + + // An alternative is to use ActivityManager.getMemoryInfo(), but this + // requires a valid ActivityManager handle, which can only come from + // a valid Context object, which itself cannot be retrieved + // during early startup, where this method is called. And making it + // an explicit parameter here makes all call paths _much_ more + // complicated. + + Pattern pattern = Pattern.compile("^MemTotal:\\s+([0-9]+) kB$"); + // Synchronously reading files in /proc in the UI thread is safe. + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); + try { + FileReader fileReader = new FileReader("/proc/meminfo"); + try { + BufferedReader reader = new BufferedReader(fileReader); + try { + String line; + for (;;) { + line = reader.readLine(); + if (line == null) { + Log.w(TAG, "/proc/meminfo lacks a MemTotal entry?"); + break; + } + Matcher m = pattern.matcher(line); + if (!m.find()) continue; + + int totalMemoryKB = Integer.parseInt(m.group(1)); + // Sanity check. + if (totalMemoryKB <= 1024) { + Log.w(TAG, "Invalid /proc/meminfo total size in kB: " + m.group(1)); + break; + } + + return totalMemoryKB; + } + + } finally { + reader.close(); + } + } finally { + fileReader.close(); + } + } catch (Exception e) { + Log.w(TAG, "Cannot get total physical size from /proc/meminfo", e); + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + + return 0; + } + + /** + * @return Whether or not this device should be considered a low end device. + */ + @CalledByNative + public static boolean isLowEndDevice() { + if (sLowEndDevice == null) { + sLowEndDevice = detectLowEndDevice(); + } + return sLowEndDevice.booleanValue(); + } + + /** + * @return Whether or not this device should be considered a low end device. + */ + public static int amountOfPhysicalMemoryKB() { + if (sAmountOfPhysicalMemoryKB == null) { + sAmountOfPhysicalMemoryKB = detectAmountOfPhysicalMemoryKB(); + } + return sAmountOfPhysicalMemoryKB.intValue(); + } + + /** + * @return Whether or not the system has low available memory. + */ + @CalledByNative + public static boolean isCurrentlyLowMemory() { + ActivityManager am = + (ActivityManager) ContextUtils.getApplicationContext().getSystemService( + Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo(); + am.getMemoryInfo(info); + return info.lowMemory; + } + + /** + * Resets the cached value, if any. + */ + @VisibleForTesting + public static void resetForTesting() { + sLowEndDevice = null; + sAmountOfPhysicalMemoryKB = null; + } + + public static boolean hasCamera(final Context context) { + final PackageManager pm = context.getPackageManager(); + // JellyBean support. + boolean hasCamera = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + hasCamera |= pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + } + return hasCamera; + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private static boolean detectLowEndDevice() { + assert CommandLine.isInitialized(); + if (CommandLine.getInstance().hasSwitch(BaseSwitches.ENABLE_LOW_END_DEVICE_MODE)) { + return true; + } + if (CommandLine.getInstance().hasSwitch(BaseSwitches.DISABLE_LOW_END_DEVICE_MODE)) { + return false; + } + + sAmountOfPhysicalMemoryKB = detectAmountOfPhysicalMemoryKB(); + boolean isLowEnd = true; + if (sAmountOfPhysicalMemoryKB <= 0) { + isLowEnd = false; + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + isLowEnd = sAmountOfPhysicalMemoryKB / 1024 <= ANDROID_O_LOW_MEMORY_DEVICE_THRESHOLD_MB; + } else { + isLowEnd = sAmountOfPhysicalMemoryKB / 1024 <= ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB; + } + + // For evaluation purposes check whether our computation agrees with Android API value. + Context appContext = ContextUtils.getApplicationContext(); + boolean isLowRam = false; + if (appContext != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + isLowRam = ((ActivityManager) ContextUtils.getApplicationContext().getSystemService( + Context.ACTIVITY_SERVICE)) + .isLowRamDevice(); + } + sLowEndMatches.record(isLowEnd == isLowRam); + + return isLowEnd; + } + + /** + * Creates a new trace event to log the number of minor / major page faults, if tracing is + * enabled. + */ + public static void logPageFaultCountToTracing() { + nativeLogPageFaultCountToTracing(); + } + + private static native void nativeLogPageFaultCountToTracing(); +} diff --git a/base/android/java/src/org/chromium/base/ThrowUncaughtException.java b/base/android/java/src/org/chromium/base/ThrowUncaughtException.java new file mode 100644 index 0000000000..d5f18a278d --- /dev/null +++ b/base/android/java/src/org/chromium/base/ThrowUncaughtException.java @@ -0,0 +1,21 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.MainDex; + +@MainDex +abstract class ThrowUncaughtException { + @CalledByNative + private static void post() { + ThreadUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + throw new RuntimeException("Intentional exception not caught by JNI"); + } + }); + } +} diff --git a/base/android/java/src/org/chromium/base/TimeUtils.java b/base/android/java/src/org/chromium/base/TimeUtils.java new file mode 100644 index 0000000000..dcacabf205 --- /dev/null +++ b/base/android/java/src/org/chromium/base/TimeUtils.java @@ -0,0 +1,18 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.MainDex; + +/** Time-related utilities. */ +@JNINamespace("base::android") +@MainDex +public class TimeUtils { + private TimeUtils() {} + + /** Returns TimeTicks::Now() in microseconds. */ + public static native long nativeGetTimeTicksNowUs(); +} diff --git a/base/android/java/src/org/chromium/base/TraceEvent.java b/base/android/java/src/org/chromium/base/TraceEvent.java new file mode 100644 index 0000000000..96590900e0 --- /dev/null +++ b/base/android/java/src/org/chromium/base/TraceEvent.java @@ -0,0 +1,387 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.os.Looper; +import android.os.MessageQueue; +import android.os.SystemClock; +import android.util.Log; +import android.util.Printer; + +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.MainDex; +/** + * Java mirror of Chrome trace event API. See base/trace_event/trace_event.h. + * + * To get scoped trace events, use the "try with resource" construct, for instance: + * <pre>{@code + * try (TraceEvent e = TraceEvent.scoped("MyTraceEvent")) { + * // code. + * } + * }</pre> + * + * It is OK to use tracing before the native library has loaded, in a slightly restricted fashion. + * @see EarlyTraceEvent for details. + */ +@JNINamespace("base::android") +@MainDex +public class TraceEvent implements AutoCloseable { + private static volatile boolean sEnabled; + private static volatile boolean sATraceEnabled; // True when taking an Android systrace. + + private static class BasicLooperMonitor implements Printer { + private static final String EARLY_TOPLEVEL_TASK_NAME = "Looper.dispatchMessage: "; + + @Override + public void println(final String line) { + if (line.startsWith(">")) { + beginHandling(line); + } else { + assert line.startsWith("<"); + endHandling(line); + } + } + + void beginHandling(final String line) { + // May return an out-of-date value. this is not an issue as EarlyTraceEvent#begin() + // will filter the event in this case. + boolean earlyTracingActive = EarlyTraceEvent.isActive(); + if (sEnabled || earlyTracingActive) { + String target = getTarget(line); + if (sEnabled) { + nativeBeginToplevel(target); + } else if (earlyTracingActive) { + // Synthesize a task name instead of using a parameter, as early tracing doesn't + // support parameters. + EarlyTraceEvent.begin(EARLY_TOPLEVEL_TASK_NAME + target); + } + } + } + + void endHandling(final String line) { + if (EarlyTraceEvent.isActive()) { + EarlyTraceEvent.end(EARLY_TOPLEVEL_TASK_NAME + getTarget(line)); + } + if (sEnabled) nativeEndToplevel(); + } + + /** + * Android Looper formats |line| as ">>>>> Dispatching to (TARGET) [...]" since at least + * 2009 (Donut). Extracts the TARGET part of the message. + */ + private static String getTarget(String logLine) { + int start = logLine.indexOf('(', 21); // strlen(">>>>> Dispatching to ") + int end = start == -1 ? -1 : logLine.indexOf(')', start); + return end != -1 ? logLine.substring(start + 1, end) : ""; + } + } + + /** + * A class that records, traces and logs statistics about the UI thead's Looper. + * The output of this class can be used in a number of interesting ways: + * <p> + * <ol><li> + * When using chrometrace, there will be a near-continuous line of + * measurements showing both event dispatches as well as idles; + * </li><li> + * Logging messages are output for events that run too long on the + * event dispatcher, making it easy to identify problematic areas; + * </li><li> + * Statistics are output whenever there is an idle after a non-trivial + * amount of activity, allowing information to be gathered about task + * density and execution cadence on the Looper; + * </li></ol> + * <p> + * The class attaches itself as an idle handler to the main Looper, and + * monitors the execution of events and idle notifications. Task counters + * accumulate between idle notifications and get reset when a new idle + * notification is received. + */ + private static final class IdleTracingLooperMonitor extends BasicLooperMonitor + implements MessageQueue.IdleHandler { + // Tags for dumping to logcat or TraceEvent + private static final String TAG = "TraceEvent.LooperMonitor"; + private static final String IDLE_EVENT_NAME = "Looper.queueIdle"; + + // Calculation constants + private static final long FRAME_DURATION_MILLIS = 1000L / 60L; // 60 FPS + // A reasonable threshold for defining a Looper event as "long running" + private static final long MIN_INTERESTING_DURATION_MILLIS = + FRAME_DURATION_MILLIS; + // A reasonable threshold for a "burst" of tasks on the Looper + private static final long MIN_INTERESTING_BURST_DURATION_MILLIS = + MIN_INTERESTING_DURATION_MILLIS * 3; + + // Stats tracking + private long mLastIdleStartedAt; + private long mLastWorkStartedAt; + private int mNumTasksSeen; + private int mNumIdlesSeen; + private int mNumTasksSinceLastIdle; + + // State + private boolean mIdleMonitorAttached; + + // Called from within the begin/end methods only. + // This method can only execute on the looper thread, because that is + // the only thread that is permitted to call Looper.myqueue(). + private final void syncIdleMonitoring() { + if (sEnabled && !mIdleMonitorAttached) { + // approximate start time for computational purposes + mLastIdleStartedAt = SystemClock.elapsedRealtime(); + Looper.myQueue().addIdleHandler(this); + mIdleMonitorAttached = true; + Log.v(TAG, "attached idle handler"); + } else if (mIdleMonitorAttached && !sEnabled) { + Looper.myQueue().removeIdleHandler(this); + mIdleMonitorAttached = false; + Log.v(TAG, "detached idle handler"); + } + } + + @Override + final void beginHandling(final String line) { + // Close-out any prior 'idle' period before starting new task. + if (mNumTasksSinceLastIdle == 0) { + TraceEvent.end(IDLE_EVENT_NAME); + } + mLastWorkStartedAt = SystemClock.elapsedRealtime(); + syncIdleMonitoring(); + super.beginHandling(line); + } + + @Override + final void endHandling(final String line) { + final long elapsed = SystemClock.elapsedRealtime() + - mLastWorkStartedAt; + if (elapsed > MIN_INTERESTING_DURATION_MILLIS) { + traceAndLog(Log.WARN, "observed a task that took " + + elapsed + "ms: " + line); + } + super.endHandling(line); + syncIdleMonitoring(); + mNumTasksSeen++; + mNumTasksSinceLastIdle++; + } + + private static void traceAndLog(int level, String message) { + TraceEvent.instant("TraceEvent.LooperMonitor:IdleStats", message); + Log.println(level, TAG, message); + } + + @Override + public final boolean queueIdle() { + final long now = SystemClock.elapsedRealtime(); + if (mLastIdleStartedAt == 0) mLastIdleStartedAt = now; + final long elapsed = now - mLastIdleStartedAt; + mNumIdlesSeen++; + TraceEvent.begin(IDLE_EVENT_NAME, mNumTasksSinceLastIdle + " tasks since last idle."); + if (elapsed > MIN_INTERESTING_BURST_DURATION_MILLIS) { + // Dump stats + String statsString = mNumTasksSeen + " tasks and " + + mNumIdlesSeen + " idles processed so far, " + + mNumTasksSinceLastIdle + " tasks bursted and " + + elapsed + "ms elapsed since last idle"; + traceAndLog(Log.DEBUG, statsString); + } + mLastIdleStartedAt = now; + mNumTasksSinceLastIdle = 0; + return true; // stay installed + } + } + + // Holder for monitor avoids unnecessary construction on non-debug runs + private static final class LooperMonitorHolder { + private static final BasicLooperMonitor sInstance = + CommandLine.getInstance().hasSwitch(BaseSwitches.ENABLE_IDLE_TRACING) + ? new IdleTracingLooperMonitor() : new BasicLooperMonitor(); + } + + private final String mName; + + /** + * Constructor used to support the "try with resource" construct. + */ + private TraceEvent(String name, String arg) { + mName = name; + begin(name, arg); + } + + @Override + public void close() { + end(mName); + } + + /** + * Factory used to support the "try with resource" construct. + * + * Note that if tracing is not enabled, this will not result in allocating an object. + * + * @param name Trace event name. + * @param name The arguments of the event. + * @return a TraceEvent, or null if tracing is not enabled. + */ + public static TraceEvent scoped(String name, String arg) { + if (!(EarlyTraceEvent.enabled() || enabled())) return null; + return new TraceEvent(name, arg); + } + + /** + * Similar to {@link #scoped(String, String arg)}, but uses null for |arg|. + */ + public static TraceEvent scoped(String name) { + return scoped(name, null); + } + + /** + * Register an enabled observer, such that java traces are always enabled with native. + */ + public static void registerNativeEnabledObserver() { + nativeRegisterEnabledObserver(); + } + + /** + * Notification from native that tracing is enabled/disabled. + */ + @CalledByNative + public static void setEnabled(boolean enabled) { + if (enabled) EarlyTraceEvent.disable(); + // Only disable logging if Chromium enabled it originally, so as to not disrupt logging done + // by other applications + if (sEnabled != enabled) { + sEnabled = enabled; + // Android M+ systrace logs this on its own. Only log it if not writing to Android + // systrace. + if (sATraceEnabled) return; + ThreadUtils.getUiThreadLooper().setMessageLogging( + enabled ? LooperMonitorHolder.sInstance : null); + } + } + + /** + * May enable early tracing depending on the environment. + * + * Must be called after the command-line has been read. + */ + public static void maybeEnableEarlyTracing() { + EarlyTraceEvent.maybeEnable(); + if (EarlyTraceEvent.isActive()) { + ThreadUtils.getUiThreadLooper().setMessageLogging(LooperMonitorHolder.sInstance); + } + } + + /** + * Enables or disabled Android systrace path of Chrome tracing. If enabled, all Chrome + * traces will be also output to Android systrace. Because of the overhead of Android + * systrace, this is for WebView only. + */ + public static void setATraceEnabled(boolean enabled) { + if (sATraceEnabled == enabled) return; + sATraceEnabled = enabled; + if (enabled) { + // Calls TraceEvent.setEnabled(true) via + // TraceLog::EnabledStateObserver::OnTraceLogEnabled + nativeStartATrace(); + } else { + // Calls TraceEvent.setEnabled(false) via + // TraceLog::EnabledStateObserver::OnTraceLogDisabled + nativeStopATrace(); + } + } + + /** + * @return True if tracing is enabled, false otherwise. + * It is safe to call trace methods without checking if TraceEvent + * is enabled. + */ + public static boolean enabled() { + return sEnabled; + } + + /** + * Triggers the 'instant' native trace event with no arguments. + * @param name The name of the event. + */ + public static void instant(String name) { + if (sEnabled) nativeInstant(name, null); + } + + /** + * Triggers the 'instant' native trace event. + * @param name The name of the event. + * @param arg The arguments of the event. + */ + public static void instant(String name, String arg) { + if (sEnabled) nativeInstant(name, arg); + } + + /** + * Triggers the 'start' native trace event with no arguments. + * @param name The name of the event. + * @param id The id of the asynchronous event. + */ + public static void startAsync(String name, long id) { + EarlyTraceEvent.startAsync(name, id); + if (sEnabled) nativeStartAsync(name, id); + } + + /** + * Triggers the 'finish' native trace event with no arguments. + * @param name The name of the event. + * @param id The id of the asynchronous event. + */ + public static void finishAsync(String name, long id) { + EarlyTraceEvent.finishAsync(name, id); + if (sEnabled) nativeFinishAsync(name, id); + } + + /** + * Triggers the 'begin' native trace event with no arguments. + * @param name The name of the event. + */ + public static void begin(String name) { + begin(name, null); + } + + /** + * Triggers the 'begin' native trace event. + * @param name The name of the event. + * @param arg The arguments of the event. + */ + public static void begin(String name, String arg) { + EarlyTraceEvent.begin(name); + if (sEnabled) nativeBegin(name, arg); + } + + /** + * Triggers the 'end' native trace event with no arguments. + * @param name The name of the event. + */ + public static void end(String name) { + end(name, null); + } + + /** + * Triggers the 'end' native trace event. + * @param name The name of the event. + * @param arg The arguments of the event. + */ + public static void end(String name, String arg) { + EarlyTraceEvent.end(name); + if (sEnabled) nativeEnd(name, arg); + } + + private static native void nativeRegisterEnabledObserver(); + private static native void nativeStartATrace(); + private static native void nativeStopATrace(); + private static native void nativeInstant(String name, String arg); + private static native void nativeBegin(String name, String arg); + private static native void nativeEnd(String name, String arg); + private static native void nativeBeginToplevel(String target); + private static native void nativeEndToplevel(); + private static native void nativeStartAsync(String name, long id); + private static native void nativeFinishAsync(String name, long id); +} diff --git a/base/android/java/src/org/chromium/base/UnguessableToken.java b/base/android/java/src/org/chromium/base/UnguessableToken.java new file mode 100644 index 0000000000..4b1619dae8 --- /dev/null +++ b/base/android/java/src/org/chromium/base/UnguessableToken.java @@ -0,0 +1,91 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +import android.os.Parcel; +import android.os.Parcelable; + +import org.chromium.base.annotations.CalledByNative; + +/** + * This class mirrors unguessable_token.h . Since tokens are passed by value, + * we don't bother to maintain a native token. This implements Parcelable so + * that it may be sent via binder. + * + * To get one of these from native, one must start with a + * base::UnguessableToken, then create a Java object from it. See + * jni_unguessable_token.h for information. + */ +public class UnguessableToken implements Parcelable { + private final long mHigh; + private final long mLow; + + private UnguessableToken(long high, long low) { + mHigh = high; + mLow = low; + } + + @CalledByNative + private static UnguessableToken create(long high, long low) { + return new UnguessableToken(high, low); + } + + @CalledByNative + public long getHighForSerialization() { + return mHigh; + } + + @CalledByNative + public long getLowForSerialization() { + return mLow; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mHigh); + dest.writeLong(mLow); + } + + public static final Parcelable.Creator<UnguessableToken> CREATOR = + new Parcelable.Creator<UnguessableToken>() { + @Override + public UnguessableToken createFromParcel(Parcel source) { + long high = source.readLong(); + long low = source.readLong(); + if (high == 0 || low == 0) { + // Refuse to create an empty UnguessableToken. + return null; + } + return new UnguessableToken(high, low); + } + + @Override + public UnguessableToken[] newArray(int size) { + return new UnguessableToken[size]; + } + }; + + // To avoid unwieldy calls in JNI for tests, parcel and unparcel. + // TODO(liberato): It would be nice if we could include this only with a + // java driver that's linked only with unit tests, but i don't see a way + // to do that. + @CalledByNative + private UnguessableToken parcelAndUnparcelForTesting() { + Parcel parcel = Parcel.obtain(); + writeToParcel(parcel, 0); + + // Rewind the parcel and un-parcel. + parcel.setDataPosition(0); + UnguessableToken token = CREATOR.createFromParcel(parcel); + parcel.recycle(); + + return token; + } +}; diff --git a/base/android/java/src/org/chromium/base/annotations/DoNotInline.java b/base/android/java/src/org/chromium/base/annotations/DoNotInline.java new file mode 100644 index 0000000000..9252f3a79b --- /dev/null +++ b/base/android/java/src/org/chromium/base/annotations/DoNotInline.java @@ -0,0 +1,20 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotated method or class should never be inlined. + * + * The annotated method (or methods on the annotated class) are guaranteed not to be inlined by + * Proguard. Other optimizations may still apply. + */ +@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.CLASS) +public @interface DoNotInline {} diff --git a/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java b/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java new file mode 100644 index 0000000000..5bc62042d4 --- /dev/null +++ b/base/android/java/src/org/chromium/base/library_loader/LibraryLoader.java @@ -0,0 +1,829 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.library_loader; + +import static org.chromium.base.metrics.CachedMetrics.EnumeratedHistogramSample; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.Process; +import android.os.StrictMode; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.system.Os; + +import org.chromium.base.AsyncTask; +import org.chromium.base.BuildConfig; +import org.chromium.base.BuildInfo; +import org.chromium.base.CommandLine; +import org.chromium.base.ContextUtils; +import org.chromium.base.FileUtils; +import org.chromium.base.Log; +import org.chromium.base.SysUtils; +import org.chromium.base.TraceEvent; +import org.chromium.base.VisibleForTesting; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.MainDex; +import org.chromium.base.metrics.RecordHistogram; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.ZipFile; + +import javax.annotation.Nullable; + +/** + * This class provides functionality to load and register the native libraries. + * Callers are allowed to separate loading the libraries from initializing them. + * This may be an advantage for Android Webview, where the libraries can be loaded + * by the zygote process, but then needs per process initialization after the + * application processes are forked from the zygote process. + * + * The libraries may be loaded and initialized from any thread. Synchronization + * primitives are used to ensure that overlapping requests from different + * threads are handled sequentially. + * + * See also base/android/library_loader/library_loader_hooks.cc, which contains + * the native counterpart to this class. + */ +@MainDex +@JNINamespace("base::android") +public class LibraryLoader { + private static final String TAG = "LibraryLoader"; + + // Set to true to enable debug logs. + private static final boolean DEBUG = false; + + // Experience shows that on some devices, the PackageManager fails to properly extract + // native shared libraries to the /data partition at installation or upgrade time, + // which creates all kind of chaos (https://crbug.com/806998). + // + // We implement a fallback when we detect the issue by manually extracting the library + // into Chromium's own data directory, then retrying to load the new library from here. + // + // This will work for any device running K-. Starting with Android L, render processes + // cannot access the file system anymore, and extraction will always fail for them. + // However, the issue doesn't seem to appear in the field for Android L. + // + // Also, starting with M, the issue doesn't exist if shared libraries are stored + // uncompressed in the APK (as Chromium does), because the system linker can access them + // directly, and the PackageManager will thus never extract them in the first place. + static public final boolean PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION = + Build.VERSION.SDK_INT <= VERSION_CODES.KITKAT; + + // Location of extracted native libraries. + private static final String LIBRARY_DIR = "native_libraries"; + + // SharedPreferences key for "don't prefetch libraries" flag + private static final String DONT_PREFETCH_LIBRARIES_KEY = "dont_prefetch_libraries"; + + private static final EnumeratedHistogramSample sRelinkerCountHistogram = + new EnumeratedHistogramSample("ChromiumAndroidLinker.RelinkerFallbackCount", 2); + + // The singleton instance of LibraryLoader. Never null (not final for tests). + private static LibraryLoader sInstance = new LibraryLoader(); + + // One-way switch becomes true when the libraries are initialized ( + // by calling nativeLibraryLoaded, which forwards to LibraryLoaded(...) in + // library_loader_hooks.cc). + // Note that this member should remain a one-way switch, since it accessed from multiple + // threads without a lock. + private volatile boolean mInitialized; + + // One-way switch that becomes true once + // {@link asyncPrefetchLibrariesToMemory} has been called. + private final AtomicBoolean mPrefetchLibraryHasBeenCalled = new AtomicBoolean(); + + // Guards all fields below. + private final Object mLock = new Object(); + + private NativeLibraryPreloader mLibraryPreloader; + private boolean mLibraryPreloaderCalled; + + // One-way switch becomes true when the libraries are loaded. + private boolean mLoaded; + + // One-way switch becomes true when the Java command line is switched to + // native. + private boolean mCommandLineSwitched; + + // One-way switches recording attempts to use Relro sharing in the browser. + // The flags are used to report UMA stats later. + private boolean mIsUsingBrowserSharedRelros; + private boolean mLoadAtFixedAddressFailed; + + // One-way switch becomes true if the Chromium library was loaded from the + // APK file directly. + private boolean mLibraryWasLoadedFromApk; + + // The type of process the shared library is loaded in. + private @LibraryProcessType int mLibraryProcessType; + + // The number of milliseconds it took to load all the native libraries, which + // will be reported via UMA. Set once when the libraries are done loading. + private long mLibraryLoadTimeMs; + + // The return value of NativeLibraryPreloader.loadLibrary(), which will be reported + // via UMA, it is initialized to the invalid value which shouldn't showup in UMA + // report. + private int mLibraryPreloaderStatus = -1; + + /** + * Call this method to determine if this chromium project must + * use this linker. If not, System.loadLibrary() should be used to load + * libraries instead. + */ + public static boolean useCrazyLinker() { + // TODO(digit): Remove this early return GVR is loadable. + // A non-monochrome APK (such as ChromePublic.apk or ChromeModernPublic.apk) on N+ cannot + // use the Linker because the latter is incompatible with the GVR library. Fall back + // to using System.loadLibrary() or System.load() at the cost of no RELRO sharing. + // + // A non-monochrome APK (such as ChromePublic.apk) can be installed on N+ in these + // circumstances: + // * installing APK manually + // * after OTA from M to N + // * side-installing Chrome (possibly from another release channel) + // * Play Store bugs leading to incorrect APK flavor being installed + // + if (Build.VERSION.SDK_INT >= VERSION_CODES.N) return false; + + // The auto-generated NativeLibraries.sUseLinker variable will be true if the + // build has not explicitly disabled Linker features. + return NativeLibraries.sUseLinker; + } + + /** + * Call this method to determine if the chromium project must load the library + * directly from a zip file. + */ + private static boolean isInZipFile() { + // The auto-generated NativeLibraries.sUseLibraryInZipFile variable will be true + // iff the library remains embedded in the APK zip file on the target. + return NativeLibraries.sUseLibraryInZipFile; + } + + /** + * Set native library preloader, if set, the NativeLibraryPreloader.loadLibrary will be invoked + * before calling System.loadLibrary, this only applies when not using the chromium linker. + * + * @param loader the NativeLibraryPreloader, it shall only be set once and before the + * native library loaded. + */ + public void setNativeLibraryPreloader(NativeLibraryPreloader loader) { + synchronized (mLock) { + assert mLibraryPreloader == null && !mLoaded; + mLibraryPreloader = loader; + } + } + + public static LibraryLoader getInstance() { + return sInstance; + } + + private LibraryLoader() {} + + /** + * This method blocks until the library is fully loaded and initialized. + * + * @param processType the process the shared library is loaded in. + */ + public void ensureInitialized(@LibraryProcessType int processType) throws ProcessInitException { + synchronized (mLock) { + if (mInitialized) { + // Already initialized, nothing to do. + return; + } + loadAlreadyLocked(ContextUtils.getApplicationContext()); + initializeAlreadyLocked(processType); + } + } + + /** + * Calls native library preloader (see {@link #setNativeLibraryPreloader}) with the app + * context. If there is no preloader set, this function does nothing. + * Preloader is called only once, so calling it explicitly via this method means + * that it won't be (implicitly) called during library loading. + */ + public void preloadNow() { + preloadNowOverrideApplicationContext(ContextUtils.getApplicationContext()); + } + + /** + * Similar to {@link #preloadNow}, but allows specifying app context to use. + */ + public void preloadNowOverrideApplicationContext(Context appContext) { + synchronized (mLock) { + if (!useCrazyLinker()) { + preloadAlreadyLocked(appContext); + } + } + } + + private void preloadAlreadyLocked(Context appContext) { + try (TraceEvent te = TraceEvent.scoped("LibraryLoader.preloadAlreadyLocked")) { + // Preloader uses system linker, we shouldn't preload if Chromium linker is used. + assert !useCrazyLinker(); + if (mLibraryPreloader != null && !mLibraryPreloaderCalled) { + mLibraryPreloaderStatus = mLibraryPreloader.loadLibrary(appContext); + mLibraryPreloaderCalled = true; + } + } + } + + /** + * Checks if library is fully loaded and initialized. + */ + public boolean isInitialized() { + return mInitialized; + } + + /** + * Loads the library and blocks until the load completes. The caller is responsible + * for subsequently calling ensureInitialized(). + * May be called on any thread, but should only be called once. Note the thread + * this is called on will be the thread that runs the native code's static initializers. + * See the comment in doInBackground() for more considerations on this. + * + * @throws ProcessInitException if the native library failed to load. + */ + public void loadNow() throws ProcessInitException { + loadNowOverrideApplicationContext(ContextUtils.getApplicationContext()); + } + + /** + * Override kept for callers that need to load from a different app context. Do not use unless + * specifically required to load from another context that is not the current process's app + * context. + * + * @param appContext The overriding app context to be used to load libraries. + * @throws ProcessInitException if the native library failed to load with this context. + */ + public void loadNowOverrideApplicationContext(Context appContext) throws ProcessInitException { + synchronized (mLock) { + if (mLoaded && appContext != ContextUtils.getApplicationContext()) { + throw new IllegalStateException("Attempt to load again from alternate context."); + } + loadAlreadyLocked(appContext); + } + } + + /** + * Initializes the library here and now: must be called on the thread that the + * native will call its "main" thread. The library must have previously been + * loaded with loadNow. + * + * @param processType the process the shared library is loaded in. + */ + public void initialize(@LibraryProcessType int processType) throws ProcessInitException { + synchronized (mLock) { + initializeAlreadyLocked(processType); + } + } + + /** + * Disables prefetching for subsequent runs. The value comes from "DontPrefetchLibraries" + * finch experiment, and is pushed on every run. I.e. the effect of the finch experiment + * lags by one run, which is the best we can do considering that prefetching happens way + * before finch is initialized. Note that since LibraryLoader is in //base, it can't depend + * on ChromeFeatureList, and has to rely on external code pushing the value. + * + * @param dontPrefetch whether not to prefetch libraries + */ + public static void setDontPrefetchLibrariesOnNextRuns(boolean dontPrefetch) { + ContextUtils.getAppSharedPreferences() + .edit() + .putBoolean(DONT_PREFETCH_LIBRARIES_KEY, dontPrefetch) + .apply(); + } + + /** + * @return whether not to prefetch libraries (see setDontPrefetchLibrariesOnNextRun()). + */ + private static boolean isNotPrefetchingLibraries() { + // This might be the first time getAppSharedPreferences() is used, so relax strict mode + // to allow disk reads. + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); + try { + return ContextUtils.getAppSharedPreferences().getBoolean( + DONT_PREFETCH_LIBRARIES_KEY, false); + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + } + + /** Prefetches the native libraries in a background thread. + * + * Launches an AsyncTask that, through a short-lived forked process, reads a + * part of each page of the native library. This is done to warm up the + * page cache, turning hard page faults into soft ones. + * + * This is done this way, as testing shows that fadvise(FADV_WILLNEED) is + * detrimental to the startup time. + */ + public void asyncPrefetchLibrariesToMemory() { + SysUtils.logPageFaultCountToTracing(); + if (isNotPrefetchingLibraries()) return; + + final boolean coldStart = mPrefetchLibraryHasBeenCalled.compareAndSet(false, true); + + // Collection should start close to the native library load, but doesn't have + // to be simultaneous with it. Also, don't prefetch in this case, as this would + // skew the results. + if (coldStart && CommandLine.getInstance().hasSwitch("log-native-library-residency")) { + // nativePeriodicallyCollectResidency() sleeps, run it on another thread, + // and not on the AsyncTask thread pool. + new Thread(LibraryLoader::nativePeriodicallyCollectResidency).start(); + return; + } + + new LibraryPrefetchTask(coldStart).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private static class LibraryPrefetchTask extends AsyncTask<Void, Void, Void> { + private final boolean mColdStart; + + public LibraryPrefetchTask(boolean coldStart) { + mColdStart = coldStart; + } + + @Override + protected Void doInBackground(Void... params) { + try (TraceEvent e = TraceEvent.scoped("LibraryLoader.asyncPrefetchLibrariesToMemory")) { + int percentage = nativePercentageOfResidentNativeLibraryCode(); + // Arbitrary percentage threshold. If most of the native library is already + // resident (likely with monochrome), don't bother creating a prefetch process. + boolean prefetch = mColdStart && percentage < 90; + if (prefetch) { + nativeForkAndPrefetchNativeLibrary(); + } + if (percentage != -1) { + String histogram = "LibraryLoader.PercentageOfResidentCodeBeforePrefetch" + + (mColdStart ? ".ColdStartup" : ".WarmStartup"); + RecordHistogram.recordPercentageHistogram(histogram, percentage); + } + } + return null; + } + } + + // Helper for loadAlreadyLocked(). Load a native shared library with the Chromium linker. + // Sets UMA flags depending on the results of loading. + private void loadLibraryWithCustomLinkerAlreadyLocked( + Linker linker, @Nullable String zipFilePath, String libFilePath) { + assert Thread.holdsLock(mLock); + if (linker.isUsingBrowserSharedRelros()) { + // If the browser is set to attempt shared RELROs then we try first with shared + // RELROs enabled, and if that fails then retry without. + mIsUsingBrowserSharedRelros = true; + try { + linker.loadLibrary(libFilePath); + } catch (UnsatisfiedLinkError e) { + Log.w(TAG, "Failed to load native library with shared RELRO, retrying without"); + mLoadAtFixedAddressFailed = true; + linker.loadLibraryNoFixedAddress(libFilePath); + } + } else { + // No attempt to use shared RELROs in the browser, so load as normal. + linker.loadLibrary(libFilePath); + } + + // Loaded successfully, so record if we loaded directly from an APK. + if (zipFilePath != null) { + mLibraryWasLoadedFromApk = true; + } + } + + static void incrementRelinkerCountHitHistogram() { + sRelinkerCountHistogram.record(1); + } + + static void incrementRelinkerCountNotHitHistogram() { + sRelinkerCountHistogram.record(0); + } + + // Experience shows that on some devices, the system sometimes fails to extract native libraries + // at installation or update time from the APK. This function will extract the library and + // return the extracted file path. + static String getExtractedLibraryPath(Context appContext, String libName) { + assert PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION; + Log.w(TAG, "Failed to load libName %s, attempting fallback extraction then trying again", + libName); + String libraryEntry = LibraryLoader.makeLibraryPathInZipFile(libName, false, false); + return extractFileIfStale(appContext, libraryEntry, makeLibraryDirAndSetPermission()); + } + + // Invoke either Linker.loadLibrary(...), System.loadLibrary(...) or System.load(...), + // triggering JNI_OnLoad in native code. + // TODO(crbug.com/635567): Fix this properly. + @SuppressLint({"DefaultLocale", "NewApi", "UnsafeDynamicallyLoadedCode"}) + private void loadAlreadyLocked(Context appContext) throws ProcessInitException { + try (TraceEvent te = TraceEvent.scoped("LibraryLoader.loadAlreadyLocked")) { + if (!mLoaded) { + assert !mInitialized; + + long startTime = SystemClock.uptimeMillis(); + + if (useCrazyLinker()) { + // Load libraries using the Chromium linker. + Linker linker = Linker.getInstance(); + + String apkFilePath = + isInZipFile() ? appContext.getApplicationInfo().sourceDir : null; + linker.prepareLibraryLoad(apkFilePath); + + for (String library : NativeLibraries.LIBRARIES) { + // Don't self-load the linker. This is because the build system is + // not clever enough to understand that all the libraries packaged + // in the final .apk don't need to be explicitly loaded. + if (linker.isChromiumLinkerLibrary(library)) { + if (DEBUG) Log.i(TAG, "ignoring self-linker load"); + continue; + } + + // Determine where the library should be loaded from. + String libFilePath = System.mapLibraryName(library); + if (apkFilePath != null) { + Log.i(TAG, " Loading " + library + " from within " + apkFilePath); + } else { + Log.i(TAG, "Loading " + library); + } + + try { + // Load the library using this Linker. May throw UnsatisfiedLinkError. + loadLibraryWithCustomLinkerAlreadyLocked( + linker, apkFilePath, libFilePath); + incrementRelinkerCountNotHitHistogram(); + } catch (UnsatisfiedLinkError e) { + if (!isInZipFile() + && PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION) { + loadLibraryWithCustomLinkerAlreadyLocked( + linker, null, getExtractedLibraryPath(appContext, library)); + incrementRelinkerCountHitHistogram(); + } else { + Log.e(TAG, "Unable to load library: " + library); + throw(e); + } + } + } + + linker.finishLibraryLoad(); + } else { + setEnvForNative(); + preloadAlreadyLocked(appContext); + + // If the libraries are located in the zip file, assert that the device API + // level is M or higher. On devices lower than M, the libraries should + // always be loaded by Linker. + assert !isInZipFile() || Build.VERSION.SDK_INT >= VERSION_CODES.M; + + // Load libraries using the system linker. + for (String library : NativeLibraries.LIBRARIES) { + try { + if (!isInZipFile()) { + // The extract and retry logic isn't needed because this path is + // used only for local development. + System.loadLibrary(library); + } else { + // Load directly from the APK. + boolean is64Bit = Process.is64Bit(); + String zipFilePath = appContext.getApplicationInfo().sourceDir; + // In API level 23 and above, it’s possible to open a .so file + // directly from the APK of the path form + // "my_zip_file.zip!/libs/libstuff.so". See: + // https://android.googlesource.com/platform/bionic/+/master/android-changes-for-ndk-developers.md#opening-shared-libraries-directly-from-an-apk + String libraryName = zipFilePath + "!/" + + makeLibraryPathInZipFile(library, true, is64Bit); + Log.i(TAG, "libraryName: " + libraryName); + System.load(libraryName); + } + } catch (UnsatisfiedLinkError e) { + Log.e(TAG, "Unable to load library: " + library); + throw(e); + } + } + } + + long stopTime = SystemClock.uptimeMillis(); + mLibraryLoadTimeMs = stopTime - startTime; + Log.i(TAG, String.format("Time to load native libraries: %d ms (timestamps %d-%d)", + mLibraryLoadTimeMs, + startTime % 10000, + stopTime % 10000)); + + mLoaded = true; + } + } catch (UnsatisfiedLinkError e) { + throw new ProcessInitException(LoaderErrors.LOADER_ERROR_NATIVE_LIBRARY_LOAD_FAILED, e); + } + } + + /** + * @param library The library name that is looking for. + * @param crazyPrefix true iff adding crazy linker prefix to the file name. + * @param is64Bit true if the caller think it's run on a 64 bit device. + * @return the library path name in the zip file. + */ + @NonNull + public static String makeLibraryPathInZipFile( + String library, boolean crazyPrefix, boolean is64Bit) { + // Determine the ABI string that Android uses to find native libraries. Values are described + // in: https://developer.android.com/ndk/guides/abis.html + // The 'armeabi' is omitted here because it is not supported in Chrome/WebView, while Cronet + // and Cast load the native library via other paths. + String cpuAbi; + switch (NativeLibraries.sCpuFamily) { + case NativeLibraries.CPU_FAMILY_ARM: + cpuAbi = is64Bit ? "arm64-v8a" : "armeabi-v7a"; + break; + case NativeLibraries.CPU_FAMILY_X86: + cpuAbi = is64Bit ? "x86_64" : "x86"; + break; + case NativeLibraries.CPU_FAMILY_MIPS: + cpuAbi = is64Bit ? "mips64" : "mips"; + break; + default: + throw new RuntimeException("Unknown CPU ABI for native libraries"); + } + + // When both the Chromium linker and zip-uncompressed native libraries are used, + // the build system renames the native shared libraries with a 'crazy.' prefix + // (e.g. "/lib/armeabi-v7a/libfoo.so" -> "/lib/armeabi-v7a/crazy.libfoo.so"). + // + // This prevents the package manager from extracting them at installation/update time + // to the /data directory. The libraries can still be accessed directly by the Chromium + // linker from the APK. + String crazyPart = crazyPrefix ? "crazy." : ""; + return String.format("lib/%s/%s%s", cpuAbi, crazyPart, System.mapLibraryName(library)); + } + + // The WebView requires the Command Line to be switched over before + // initialization is done. This is okay in the WebView's case since the + // JNI is already loaded by this point. + public void switchCommandLineForWebView() { + synchronized (mLock) { + ensureCommandLineSwitchedAlreadyLocked(); + } + } + + // Switch the CommandLine over from Java to native if it hasn't already been done. + // This must happen after the code is loaded and after JNI is ready (since after the + // switch the Java CommandLine will delegate all calls the native CommandLine). + private void ensureCommandLineSwitchedAlreadyLocked() { + assert mLoaded; + if (mCommandLineSwitched) { + return; + } + CommandLine.enableNativeProxy(); + mCommandLineSwitched = true; + } + + // Invoke base::android::LibraryLoaded in library_loader_hooks.cc + private void initializeAlreadyLocked(@LibraryProcessType int processType) + throws ProcessInitException { + if (mInitialized) { + if (mLibraryProcessType != processType) { + throw new ProcessInitException( + LoaderErrors.LOADER_ERROR_NATIVE_LIBRARY_LOAD_FAILED); + } + return; + } + mLibraryProcessType = processType; + + ensureCommandLineSwitchedAlreadyLocked(); + + if (!nativeLibraryLoaded(mLibraryProcessType)) { + Log.e(TAG, "error calling nativeLibraryLoaded"); + throw new ProcessInitException(LoaderErrors.LOADER_ERROR_FAILED_TO_REGISTER_JNI); + } + + // Check that the version of the library we have loaded matches the version we expect + Log.i(TAG, String.format("Expected native library version number \"%s\", " + + "actual native library version number \"%s\"", + NativeLibraries.sVersionNumber, nativeGetVersionNumber())); + if (!NativeLibraries.sVersionNumber.equals(nativeGetVersionNumber())) { + throw new ProcessInitException(LoaderErrors.LOADER_ERROR_NATIVE_LIBRARY_WRONG_VERSION); + } + + // From now on, keep tracing in sync with native. + TraceEvent.registerNativeEnabledObserver(); + + if (processType == LibraryProcessType.PROCESS_BROWSER + && PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION) { + // Perform the detection and deletion of obsolete native libraries on a background + // background thread. + AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() { + @Override + public void run() { + final String suffix = BuildInfo.getInstance().extractedFileSuffix; + final File[] files = getLibraryDir().listFiles(); + if (files == null) return; + + for (File file : files) { + // NOTE: Do not simply look for <suffix> at the end of the file. + // + // Extracted library files have names like 'libfoo.so<suffix>', but + // extractFileIfStale() will use FileUtils.copyFileStreamAtomicWithBuffer() + // to create them, and this method actually uses a transient temporary file + // named like 'libfoo.so<suffix>.tmp' to do that. These temporary files, if + // detected here, should be preserved; hence the reason why contains() is + // used below. + if (!file.getName().contains(suffix)) { + String fileName = file.getName(); + if (!file.delete()) { + Log.w(TAG, "Unable to remove %s", fileName); + } else { + Log.i(TAG, "Removed obsolete file %s", fileName); + } + } + } + } + }); + } + + // From this point on, native code is ready to use and checkIsReady() + // shouldn't complain from now on (and in fact, it's used by the + // following calls). + // Note that this flag can be accessed asynchronously, so any initialization + // must be performed before. + mInitialized = true; + } + + // Called after all native initializations are complete. + public void onNativeInitializationComplete() { + synchronized (mLock) { + recordBrowserProcessHistogramAlreadyLocked(); + } + } + + // Record Chromium linker histogram state for the main browser process. Called from + // onNativeInitializationComplete(). + private void recordBrowserProcessHistogramAlreadyLocked() { + assert Thread.holdsLock(mLock); + if (useCrazyLinker()) { + nativeRecordChromiumAndroidLinkerBrowserHistogram(mIsUsingBrowserSharedRelros, + mLoadAtFixedAddressFailed, + mLibraryWasLoadedFromApk ? LibraryLoadFromApkStatusCodes.SUCCESSFUL + : LibraryLoadFromApkStatusCodes.UNKNOWN, + mLibraryLoadTimeMs); + } + if (mLibraryPreloader != null) { + nativeRecordLibraryPreloaderBrowserHistogram(mLibraryPreloaderStatus); + } + } + + // Register pending Chromium linker histogram state for renderer processes. This cannot be + // recorded as a histogram immediately because histograms and IPC are not ready at the + // time it are captured. This function stores a pending value, so that a later call to + // RecordChromiumAndroidLinkerRendererHistogram() will record it correctly. + public void registerRendererProcessHistogram(boolean requestedSharedRelro, + boolean loadAtFixedAddressFailed) { + synchronized (mLock) { + if (useCrazyLinker()) { + nativeRegisterChromiumAndroidLinkerRendererHistogram( + requestedSharedRelro, loadAtFixedAddressFailed, mLibraryLoadTimeMs); + } + if (mLibraryPreloader != null) { + nativeRegisterLibraryPreloaderRendererHistogram(mLibraryPreloaderStatus); + } + } + } + + /** + * Override the library loader (normally with a mock) for testing. + * @param loader the mock library loader. + */ + @VisibleForTesting + public static void setLibraryLoaderForTesting(LibraryLoader loader) { + sInstance = loader; + } + + /** + * Configure ubsan using $UBSAN_OPTIONS. This function needs to be called before any native + * libraries are loaded because ubsan reads its configuration from $UBSAN_OPTIONS when the + * native library is loaded. + */ + public static void setEnvForNative() { + // The setenv API was added in L. On older versions of Android, we should still see ubsan + // reports, but they will not have stack traces. + if (BuildConfig.IS_UBSAN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + // This value is duplicated in build/android/pylib/constants/__init__.py. + Os.setenv("UBSAN_OPTIONS", + "print_stacktrace=1 stack_trace_format='#%n pc %o %m' " + + "handle_segv=0 handle_sigbus=0 handle_sigfpe=0", + true); + } catch (Exception e) { + Log.w(TAG, "failed to set UBSAN_OPTIONS", e); + } + } + } + + // Android system sometimes fails to extract libraries from APK (https://crbug.com/806998). + // This function manually extract libraries as a fallback. + @SuppressLint({"SetWorldReadable"}) + private static String extractFileIfStale( + Context appContext, String pathWithinApk, File destDir) { + assert PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION; + + String apkPath = appContext.getApplicationInfo().sourceDir; + String fileName = + (new File(pathWithinApk)).getName() + BuildInfo.getInstance().extractedFileSuffix; + File libraryFile = new File(destDir, fileName); + + if (!libraryFile.exists()) { + try (ZipFile zipFile = new ZipFile(apkPath); + InputStream inputStream = + zipFile.getInputStream(zipFile.getEntry(pathWithinApk))) { + if (zipFile.getEntry(pathWithinApk) == null) + throw new RuntimeException("Cannot find ZipEntry" + pathWithinApk); + + FileUtils.copyFileStreamAtomicWithBuffer( + inputStream, libraryFile, new byte[16 * 1024]); + libraryFile.setReadable(true, false); + libraryFile.setExecutable(true, false); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return libraryFile.getAbsolutePath(); + } + + // Ensure the extracted native libraries is created with the right permissions. + private static File makeLibraryDirAndSetPermission() { + if (!ContextUtils.isIsolatedProcess()) { + File cacheDir = ContextCompat.getCodeCacheDir(ContextUtils.getApplicationContext()); + File libDir = new File(cacheDir, LIBRARY_DIR); + cacheDir.mkdir(); + cacheDir.setExecutable(true, false); + libDir.mkdir(); + libDir.setExecutable(true, false); + } + return getLibraryDir(); + } + + // Return File object for the directory containing extracted native libraries. + private static File getLibraryDir() { + return new File( + ContextCompat.getCodeCacheDir(ContextUtils.getApplicationContext()), LIBRARY_DIR); + } + + // Only methods needed before or during normal JNI registration are during System.OnLoad. + // nativeLibraryLoaded is then called to register everything else. This process is called + // "initialization". This method will be mapped (by generated code) to the LibraryLoaded + // definition in base/android/library_loader/library_loader_hooks.cc. + // + // Return true on success and false on failure. + private native boolean nativeLibraryLoaded(@LibraryProcessType int processType); + + // Method called to record statistics about the Chromium linker operation for the main + // browser process. Indicates whether the linker attempted relro sharing for the browser, + // and if it did, whether the library failed to load at a fixed address. Also records + // support for loading a library directly from the APK file, and the number of milliseconds + // it took to load the libraries. + private native void nativeRecordChromiumAndroidLinkerBrowserHistogram( + boolean isUsingBrowserSharedRelros, + boolean loadAtFixedAddressFailed, + int libraryLoadFromApkStatus, + long libraryLoadTime); + + // Method called to record the return value of NativeLibraryPreloader.loadLibrary for the main + // browser process. + private native void nativeRecordLibraryPreloaderBrowserHistogram(int status); + + // Method called to register (for later recording) statistics about the Chromium linker + // operation for a renderer process. Indicates whether the linker attempted relro sharing, + // and if it did, whether the library failed to load at a fixed address. Also records the + // number of milliseconds it took to load the libraries. + private native void nativeRegisterChromiumAndroidLinkerRendererHistogram( + boolean requestedSharedRelro, + boolean loadAtFixedAddressFailed, + long libraryLoadTime); + + // Method called to register (for later recording) the return value of + // NativeLibraryPreloader.loadLibrary for a renderer process. + private native void nativeRegisterLibraryPreloaderRendererHistogram(int status); + + // Get the version of the native library. This is needed so that we can check we + // have the right version before initializing the (rest of the) JNI. + private native String nativeGetVersionNumber(); + + // Finds the ranges corresponding to the native library pages, forks a new + // process to prefetch these pages and waits for it. The new process then + // terminates. This is blocking. + private static native void nativeForkAndPrefetchNativeLibrary(); + + // Returns the percentage of the native library code page that are currently reseident in + // memory. + private static native int nativePercentageOfResidentNativeLibraryCode(); + + // Periodically logs native library residency from this thread. + private static native void nativePeriodicallyCollectResidency(); +} diff --git a/base/android/java/src/org/chromium/base/library_loader/Linker.java b/base/android/java/src/org/chromium/base/library_loader/Linker.java new file mode 100644 index 0000000000..5e30cfa496 --- /dev/null +++ b/base/android/java/src/org/chromium/base/library_loader/Linker.java @@ -0,0 +1,1160 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.library_loader; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +import org.chromium.base.ContextUtils; +import org.chromium.base.Log; +import org.chromium.base.StreamUtil; +import org.chromium.base.SysUtils; +import org.chromium.base.ThreadUtils; +import org.chromium.base.annotations.AccessedByNative; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.MainDex; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import javax.annotation.Nullable; + +/* + * Technical note: + * + * The point of this class is to provide an alternative to System.loadLibrary() + * to load native shared libraries. One specific feature that it supports is the + * ability to save RAM by sharing the ELF RELRO sections between renderer + * processes. + * + * When two processes load the same native library at the _same_ memory address, + * the content of their RELRO section (which includes C++ vtables or any + * constants that contain pointers) will be largely identical [1]. + * + * By default, the RELRO section is backed by private RAM in each process, + * which is still significant on mobile (e.g. 1.28 MB / process on Chrome 30 for + * Android). + * + * However, it is possible to save RAM by creating a shared memory region, + * copy the RELRO content into it, then have each process swap its private, + * regular RELRO, with a shared, read-only, mapping of the shared one. + * + * This trick saves 98% of the RELRO section size per extra process, after the + * first one. On the other hand, this requires careful communication between + * the process where the shared RELRO is created and the one(s) where it is used. + * + * Note that swapping the regular RELRO with the shared one is not an atomic + * operation. Care must be taken that no other thread tries to run native code + * that accesses it during it. In practice, this means the swap must happen + * before library native code is executed. + * + * [1] The exceptions are pointers to external, randomized, symbols, like + * those from some system libraries, but these are very few in practice. + */ + +/* + * Security considerations: + * + * - Whether the browser process loads its native libraries at the same + * addresses as the service ones (to save RAM by sharing the RELRO too) + * depends on the configuration variable BROWSER_SHARED_RELRO_CONFIG. + * + * Not using fixed library addresses in the browser process is preferred + * for regular devices since it maintains the efficacy of ASLR as an + * exploit mitigation across the render <-> browser privilege boundary. + * + * - The shared RELRO memory region is always forced read-only after creation, + * which means it is impossible for a compromised service process to map + * it read-write (e.g. by calling mmap() or mprotect()) and modify its + * content, altering values seen in other service processes. + * + * - Once the RELRO ashmem region or file is mapped into a service process's + * address space, the corresponding file descriptor is immediately closed. The + * file descriptor is kept opened in the browser process, because a copy needs + * to be sent to each new potential service process. + * + * - The common library load addresses are randomized for each instance of + * the program on the device. See getRandomBaseLoadAddress() for more + * details on how this is obtained. + * + * - When loading several libraries in service processes, a simple incremental + * approach from the original random base load address is used. This is + * sufficient to deal correctly with component builds (which can use dozens + * of shared libraries), while regular builds always embed a single shared + * library per APK. + */ + +/** + * Here's an explanation of how this class is supposed to be used: + * + * - Native shared libraries should be loaded with Linker.loadLibrary(), + * instead of System.loadLibrary(). The two functions should behave the same + * (at a high level). + * + * - Before loading any library, prepareLibraryLoad() should be called. + * + * - After loading all libraries, finishLibraryLoad() should be called, before + * running any native code from any of the libraries (except their static + * constructors, which can't be avoided). + * + * - A service process shall call either initServiceProcess() or + * disableSharedRelros() early (i.e. before any loadLibrary() call). + * Otherwise, the linker considers that it is running inside the browser + * process. This is because various Chromium projects have vastly + * different initialization paths. + * + * disableSharedRelros() completely disables shared RELROs, and loadLibrary() + * will behave exactly like System.loadLibrary(). + * + * initServiceProcess(baseLoadAddress) indicates that shared RELROs are to be + * used in this process. + * + * - The browser is in charge of deciding where in memory each library should + * be loaded. This address must be passed to each service process (see + * ChromiumLinkerParams.java in content for a helper class to do so). + * + * - The browser will also generate shared RELROs for each library it loads. + * More specifically, by default when in the browser process, the linker + * will: + * + * - Load libraries randomly (just like System.loadLibrary()). + * - Compute the fixed address to be used to load the same library + * in service processes. + * - Create a shared memory region populated with the RELRO region + * content pre-relocated for the specific fixed address above. + * + * Note that these shared RELRO regions cannot be used inside the browser + * process. They are also never mapped into it. + * + * This behaviour is altered by the BROWSER_SHARED_RELRO_CONFIG configuration + * variable below, which may force the browser to load the libraries at + * fixed addresses too. + * + * - Once all libraries are loaded in the browser process, one can call + * getSharedRelros() which returns a Bundle instance containing a map that + * links each loaded library to its shared RELRO region. + * + * This Bundle must be passed to each service process, for example through + * a Binder call (note that the Bundle includes file descriptors and cannot + * be added as an Intent extra). + * + * - In a service process, finishLibraryLoad() and/or loadLibrary() may + * block until the RELRO section Bundle is received. This is typically + * done by calling useSharedRelros() from another thread. + * + * This method also ensures the process uses the shared RELROs. + */ +public class Linker { + // Log tag for this class. + private static final String TAG = "LibraryLoader"; + + // Name of the library that contains our JNI code. + private static final String LINKER_JNI_LIBRARY = "chromium_android_linker"; + + // Constants used to control the behaviour of the browser process with + // regards to the shared RELRO section. + // NEVER -> The browser never uses it itself. + // LOW_RAM_ONLY -> It is only used on devices with low RAM. + // ALWAYS -> It is always used. + // NOTE: These names are known and expected by the Linker test scripts. + public static final int BROWSER_SHARED_RELRO_CONFIG_NEVER = 0; + public static final int BROWSER_SHARED_RELRO_CONFIG_LOW_RAM_ONLY = 1; + public static final int BROWSER_SHARED_RELRO_CONFIG_ALWAYS = 2; + + // Configuration variable used to control how the browser process uses the + // shared RELRO. Only change this while debugging linker-related issues. + // NOTE: This variable's name is known and expected by the Linker test scripts. + public static final int BROWSER_SHARED_RELRO_CONFIG = + BROWSER_SHARED_RELRO_CONFIG_LOW_RAM_ONLY; + + // Constants used to control the memory device config. Can be set explicitly + // by setMemoryDeviceConfigForTesting(). + // INIT -> Value is undetermined (will check at runtime). + // LOW -> This is a low-memory device. + // NORMAL -> This is not a low-memory device. + public static final int MEMORY_DEVICE_CONFIG_INIT = 0; + public static final int MEMORY_DEVICE_CONFIG_LOW = 1; + public static final int MEMORY_DEVICE_CONFIG_NORMAL = 2; + + // Indicates if this is a low-memory device or not. The default is to + // determine this by probing the system at runtime, but this can be forced + // for testing by calling setMemoryDeviceConfigForTesting(). + private int mMemoryDeviceConfig = MEMORY_DEVICE_CONFIG_INIT; + + // Set to true to enable debug logs. + protected static final boolean DEBUG = false; + + // Used to pass the shared RELRO Bundle through Binder. + public static final String EXTRA_LINKER_SHARED_RELROS = + "org.chromium.base.android.linker.shared_relros"; + + // Guards all access to the linker. + protected final Object mLock = new Object(); + + // The name of a class that implements TestRunner. + private String mTestRunnerClassName; + + // Size of reserved Breakpad guard region. Should match the value of + // kBreakpadGuardRegionBytes on the JNI side. Used when computing the load + // addresses of multiple loaded libraries. Set to 0 to disable the guard. + private static final int BREAKPAD_GUARD_REGION_BYTES = 16 * 1024 * 1024; + + // Size of the area requested when using ASLR to obtain a random load address. + // Should match the value of kAddressSpaceReservationSize on the JNI side. + // Used when computing the load addresses of multiple loaded libraries to + // ensure that we don't try to load outside the area originally requested. + private static final int ADDRESS_SPACE_RESERVATION = 192 * 1024 * 1024; + + // Becomes true after linker initialization. + private boolean mInitialized; + + // Set to true if this runs in the browser process. Disabled by initServiceProcess(). + private boolean mInBrowserProcess = true; + + // Becomes true to indicate this process needs to wait for a shared RELRO in + // finishLibraryLoad(). + private boolean mWaitForSharedRelros; + + // Becomes true when initialization determines that the browser process can use the + // shared RELRO. + private boolean mBrowserUsesSharedRelro; + + // The map of all RELRO sections either created or used in this process. + private Bundle mSharedRelros; + + // Current common random base load address. A value of -1 indicates not yet initialized. + private long mBaseLoadAddress = -1; + + // Current fixed-location load address for the next library called by loadLibrary(). + // A value of -1 indicates not yet initialized. + private long mCurrentLoadAddress = -1; + + // Becomes true once prepareLibraryLoad() has been called. + private boolean mPrepareLibraryLoadCalled; + + // The map of libraries that are currently loaded in this process. + private HashMap<String, LibInfo> mLoadedLibraries; + + // Singleton. + private static final Linker sSingleton = new Linker(); + + // Private singleton constructor. + private Linker() { + // Ensure this class is not referenced unless it's used. + assert LibraryLoader.useCrazyLinker(); + } + + /** + * Get singleton instance. Returns a Linker. + * + * On N+ Monochrome is selected by Play Store. With Monochrome this code is not used, instead + * Chrome asks the WebView to provide the library (and the shared RELRO). If the WebView fails + * to provide the library, the system linker is used as a fallback. + * + * Linker runs on all Android releases, but is incompatible with GVR library on N+. + * Linker is preferred on M- because it does not write the shared RELRO to disk at + * almost every cold startup. + * + * @return the Linker implementation instance. + */ + public static Linker getInstance() { + return sSingleton; + } + + /** + * Check that native library linker tests are enabled. + * If not enabled, calls to testing functions will fail with an assertion + * error. + * + * @return true if native library linker tests are enabled. + */ + public static boolean areTestsEnabled() { + return NativeLibraries.sEnableLinkerTests; + } + + /** + * Assert NativeLibraries.sEnableLinkerTests is true. + * Hard assertion that we are in a testing context. Cannot be disabled. The + * test methods in this module permit injection of runnable code by class + * name. To protect against both malicious and accidental use of these + * methods, we ensure that NativeLibraries.sEnableLinkerTests is true when + * any is called. + */ + private static void assertLinkerTestsAreEnabled() { + assert NativeLibraries.sEnableLinkerTests : "Testing method called in non-testing context"; + } + + /** + * A public interface used to run runtime linker tests after loading + * libraries. Should only be used to implement the linker unit tests, + * which is controlled by the value of NativeLibraries.sEnableLinkerTests + * configured at build time. + */ + public interface TestRunner { + /** + * Run runtime checks and return true if they all pass. + * + * @param memoryDeviceConfig The current memory device configuration. + * @param inBrowserProcess true iff this is the browser process. + * @return true if all checks pass. + */ + public boolean runChecks(int memoryDeviceConfig, boolean inBrowserProcess); + } + + /** + * Call this to retrieve the name of the current TestRunner class name + * if any. This can be useful to pass it from the browser process to + * child ones. + * + * @return null or a String holding the name of the class implementing + * the TestRunner set by calling setTestRunnerClassNameForTesting() previously. + */ + public final String getTestRunnerClassNameForTesting() { + // Sanity check. This method may only be called during tests. + assertLinkerTestsAreEnabled(); + + synchronized (mLock) { + return mTestRunnerClassName; + } + } + + /** + * Sets the test class name. + * + * On the first call, instantiates a Linker and sets its test runner class name. On subsequent + * calls, checks that the singleton produced by the first call matches the test runner class + * name. + */ + public static final void setupForTesting(String testRunnerClassName) { + if (DEBUG) { + Log.i(TAG, "setupForTesting(" + testRunnerClassName + ") called"); + } + // Sanity check. This method may only be called during tests. + assertLinkerTestsAreEnabled(); + + synchronized (sSingleton) { + sSingleton.mTestRunnerClassName = testRunnerClassName; + } + } + + /** + * Instantiate and run the current TestRunner, if any. The TestRunner implementation + * must be instantiated _after_ all libraries are loaded to ensure that its + * native methods are properly registered. + * + * @param memoryDeviceConfig Linker memory config, or 0 if unused + * @param inBrowserProcess true if in the browser process + */ + private final void runTestRunnerClassForTesting( + int memoryDeviceConfig, boolean inBrowserProcess) { + if (DEBUG) { + Log.i(TAG, "runTestRunnerClassForTesting called"); + } + // Sanity check. This method may only be called during tests. + assertLinkerTestsAreEnabled(); + + synchronized (mLock) { + if (mTestRunnerClassName == null) { + Log.wtf(TAG, "Linker runtime tests not set up for this process"); + assert false; + } + if (DEBUG) { + Log.i(TAG, "Instantiating " + mTestRunnerClassName); + } + TestRunner testRunner = null; + try { + testRunner = (TestRunner) Class.forName(mTestRunnerClassName) + .getDeclaredConstructor() + .newInstance(); + } catch (Exception e) { + Log.wtf(TAG, "Could not instantiate test runner class by name", e); + assert false; + } + + if (!testRunner.runChecks(memoryDeviceConfig, inBrowserProcess)) { + Log.wtf(TAG, "Linker runtime tests failed in this process"); + assert false; + } + + Log.i(TAG, "All linker tests passed"); + } + } + + /** + * Call this method before any other Linker method to force a specific + * memory device configuration. Should only be used for testing. + * + * @param memoryDeviceConfig MEMORY_DEVICE_CONFIG_LOW or MEMORY_DEVICE_CONFIG_NORMAL. + */ + public final void setMemoryDeviceConfigForTesting(int memoryDeviceConfig) { + if (DEBUG) { + Log.i(TAG, "setMemoryDeviceConfigForTesting(" + memoryDeviceConfig + ") called"); + } + // Sanity check. This method may only be called during tests. + assertLinkerTestsAreEnabled(); + assert memoryDeviceConfig == MEMORY_DEVICE_CONFIG_LOW + || memoryDeviceConfig == MEMORY_DEVICE_CONFIG_NORMAL; + + synchronized (mLock) { + assert mMemoryDeviceConfig == MEMORY_DEVICE_CONFIG_INIT; + + mMemoryDeviceConfig = memoryDeviceConfig; + if (DEBUG) { + if (mMemoryDeviceConfig == MEMORY_DEVICE_CONFIG_LOW) { + Log.i(TAG, "Simulating a low-memory device"); + } else { + Log.i(TAG, "Simulating a regular-memory device"); + } + } + } + } + + /** + * Determine whether a library is the linker library. + * + * @param library the name of the library. + * @return true is the library is the Linker's own JNI library. + */ + boolean isChromiumLinkerLibrary(String library) { + return library.equals(LINKER_JNI_LIBRARY); + } + + /** + * Load the Linker JNI library. Throws UnsatisfiedLinkError on error. + */ + @SuppressLint({"UnsafeDynamicallyLoadedCode"}) + private static void loadLinkerJniLibrary() { + LibraryLoader.setEnvForNative(); + if (DEBUG) { + String libName = "lib" + LINKER_JNI_LIBRARY + ".so"; + Log.i(TAG, "Loading " + libName); + } + try { + System.loadLibrary(LINKER_JNI_LIBRARY); + LibraryLoader.incrementRelinkerCountNotHitHistogram(); + } catch (UnsatisfiedLinkError e) { + if (LibraryLoader.PLATFORM_REQUIRES_NATIVE_FALLBACK_EXTRACTION) { + System.load(LibraryLoader.getExtractedLibraryPath( + ContextUtils.getApplicationContext(), LINKER_JNI_LIBRARY)); + LibraryLoader.incrementRelinkerCountHitHistogram(); + } + } + } + + /** + * Obtain a random base load address at which to place loaded libraries. + * + * @return new base load address + */ + private long getRandomBaseLoadAddress() { + // nativeGetRandomBaseLoadAddress() returns an address at which it has previously + // successfully mapped an area larger than the largest library we expect to load, + // on the basis that we will be able, with high probability, to map our library + // into it. + // + // One issue with this is that we do not yet know the size of the library that + // we will load is. If it is smaller than the size we used to obtain a random + // address the library mapping may still succeed. The other issue is that + // although highly unlikely, there is no guarantee that something else does not + // map into the area we are going to use between here and when we try to map into it. + // + // The above notes mean that all of this is probablistic. It is however okay to do + // because if, worst case and unlikely, we get unlucky in our choice of address, + // the back-out and retry without the shared RELRO in the ChildProcessService will + // keep things running. + final long address = nativeGetRandomBaseLoadAddress(); + if (DEBUG) { + Log.i(TAG, String.format(Locale.US, "Random native base load address: 0x%x", address)); + } + return address; + } + + /** + * Load a native shared library with the Chromium linker. Note the crazy linker treats + * libraries and files as equivalent, so you can only open one library in a given zip + * file. The library must not be the Chromium linker library. + * + * @param libFilePath The path of the library (possibly in the zip file). + */ + void loadLibrary(String libFilePath) { + if (DEBUG) { + Log.i(TAG, "loadLibrary: " + libFilePath); + } + final boolean isFixedAddressPermitted = true; + loadLibraryImpl(libFilePath, isFixedAddressPermitted); + } + + /** + * Load a native shared library with the Chromium linker, ignoring any + * requested fixed address for RELRO sharing. Note the crazy linker treats libraries and + * files as equivalent, so you can only open one library in a given zip file. The + * library must not be the Chromium linker library. + * + * @param libFilePath The path of the library (possibly in the zip file). + */ + void loadLibraryNoFixedAddress(String libFilePath) { + if (DEBUG) { + Log.i(TAG, "loadLibraryAtAnyAddress: " + libFilePath); + } + final boolean isFixedAddressPermitted = false; + loadLibraryImpl(libFilePath, isFixedAddressPermitted); + } + + // Used internally to initialize the linker's data. Assumes lock is held. + // Loads JNI, and sets mMemoryDeviceConfig and mBrowserUsesSharedRelro. + private void ensureInitializedLocked() { + assert Thread.holdsLock(mLock); + + if (mInitialized) { + return; + } + + // On first call, load libchromium_android_linker.so. Cannot be done in the + // constructor because instantiation occurs on the UI thread. + loadLinkerJniLibrary(); + + if (mMemoryDeviceConfig == MEMORY_DEVICE_CONFIG_INIT) { + if (SysUtils.isLowEndDevice()) { + mMemoryDeviceConfig = MEMORY_DEVICE_CONFIG_LOW; + } else { + mMemoryDeviceConfig = MEMORY_DEVICE_CONFIG_NORMAL; + } + } + + // Cannot run in the constructor because SysUtils.isLowEndDevice() relies + // on CommandLine, which may not be available at instantiation. + switch (BROWSER_SHARED_RELRO_CONFIG) { + case BROWSER_SHARED_RELRO_CONFIG_NEVER: + mBrowserUsesSharedRelro = false; + break; + case BROWSER_SHARED_RELRO_CONFIG_LOW_RAM_ONLY: + if (mMemoryDeviceConfig == MEMORY_DEVICE_CONFIG_LOW) { + mBrowserUsesSharedRelro = true; + Log.w(TAG, "Low-memory device: shared RELROs used in all processes"); + } else { + mBrowserUsesSharedRelro = false; + } + break; + case BROWSER_SHARED_RELRO_CONFIG_ALWAYS: + Log.w(TAG, "Beware: shared RELROs used in all processes!"); + mBrowserUsesSharedRelro = true; + break; + default: + Log.wtf(TAG, "FATAL: illegal shared RELRO config"); + throw new AssertionError(); + } + + mInitialized = true; + } + + /** + * Call this method to determine if the linker will try to use shared RELROs + * for the browser process. + */ + public boolean isUsingBrowserSharedRelros() { + synchronized (mLock) { + ensureInitializedLocked(); + return mInBrowserProcess && mBrowserUsesSharedRelro; + } + } + + /** + * Call this method just before loading any native shared libraries in this process. + * + * @param apkFilePath Optional current APK file path. If provided, the linker + * will try to load libraries directly from it. + */ + public void prepareLibraryLoad(@Nullable String apkFilePath) { + if (DEBUG) { + Log.i(TAG, "prepareLibraryLoad() called"); + } + synchronized (mLock) { + ensureInitializedLocked(); + if (apkFilePath != null) { + nativeAddZipArchivePath(apkFilePath); + } + mPrepareLibraryLoadCalled = true; + + if (mInBrowserProcess) { + // Force generation of random base load address, as well + // as creation of shared RELRO sections in this process. + setupBaseLoadAddressLocked(); + } + } + } + + /** + * Call this method just after loading all native shared libraries in this process. + * Note that when in a service process, this will block until the RELRO bundle is + * received, i.e. when another thread calls useSharedRelros(). + */ + void finishLibraryLoad() { + if (DEBUG) { + Log.i(TAG, "finishLibraryLoad() called"); + } + synchronized (mLock) { + ensureInitializedLocked(); + if (DEBUG) { + Log.i(TAG, + String.format(Locale.US, + "mInBrowserProcess=%b mBrowserUsesSharedRelro=%b mWaitForSharedRelros=%b", + mInBrowserProcess, mBrowserUsesSharedRelro, mWaitForSharedRelros)); + } + + if (mLoadedLibraries == null) { + if (DEBUG) { + Log.i(TAG, "No libraries loaded"); + } + } else { + if (mInBrowserProcess) { + // Create new Bundle containing RELRO section information + // for all loaded libraries. Make it available to getSharedRelros(). + mSharedRelros = createBundleFromLibInfoMap(mLoadedLibraries); + if (DEBUG) { + Log.i(TAG, "Shared RELRO created"); + dumpBundle(mSharedRelros); + } + + if (mBrowserUsesSharedRelro) { + useSharedRelrosLocked(mSharedRelros); + } + } + + if (mWaitForSharedRelros) { + assert !mInBrowserProcess; + + // Wait until the shared relro bundle is received from useSharedRelros(). + while (mSharedRelros == null) { + try { + mLock.wait(); + } catch (InterruptedException ie) { + // Restore the thread's interrupt status. + Thread.currentThread().interrupt(); + } + } + useSharedRelrosLocked(mSharedRelros); + // Clear the Bundle to ensure its file descriptor references can't be reused. + mSharedRelros.clear(); + mSharedRelros = null; + } + } + + // If testing, run tests now that all libraries are loaded and initialized. + if (NativeLibraries.sEnableLinkerTests) { + runTestRunnerClassForTesting(mMemoryDeviceConfig, mInBrowserProcess); + } + } + if (DEBUG) { + Log.i(TAG, "finishLibraryLoad() exiting"); + } + } + + /** + * Call this to send a Bundle containing the shared RELRO sections to be + * used in this process. If initServiceProcess() was previously called, + * finishLibraryLoad() will not exit until this method is called in another + * thread with a non-null value. + * + * @param bundle The Bundle instance containing a map of shared RELRO sections + * to use in this process. + */ + public void useSharedRelros(Bundle bundle) { + // Ensure the bundle uses the application's class loader, not the framework + // one which doesn't know anything about LibInfo. + // Also, hold a fresh copy of it so the caller can't recycle it. + Bundle clonedBundle = null; + if (bundle != null) { + bundle.setClassLoader(LibInfo.class.getClassLoader()); + clonedBundle = new Bundle(LibInfo.class.getClassLoader()); + Parcel parcel = Parcel.obtain(); + bundle.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + clonedBundle.readFromParcel(parcel); + parcel.recycle(); + } + if (DEBUG) { + Log.i(TAG, "useSharedRelros() called with " + bundle + ", cloned " + clonedBundle); + } + synchronized (mLock) { + // Note that in certain cases, this can be called before + // initServiceProcess() in service processes. + mSharedRelros = clonedBundle; + // Tell any listener blocked in finishLibraryLoad() about it. + mLock.notifyAll(); + } + } + + /** + * Call this to retrieve the shared RELRO sections created in this process, + * after loading all libraries. + * + * @return a new Bundle instance, or null if RELRO sharing is disabled on + * this system, or if initServiceProcess() was called previously. + */ + public Bundle getSharedRelros() { + if (DEBUG) { + Log.i(TAG, "getSharedRelros() called"); + } + synchronized (mLock) { + if (!mInBrowserProcess) { + if (DEBUG) { + Log.i(TAG, "... returning null Bundle"); + } + return null; + } + + // Return the Bundle created in finishLibraryLoad(). + if (DEBUG) { + Log.i(TAG, "... returning " + mSharedRelros); + } + return mSharedRelros; + } + } + + /** + * Call this method before loading any libraries to indicate that this + * process shall neither create or reuse shared RELRO sections. + */ + public void disableSharedRelros() { + if (DEBUG) { + Log.i(TAG, "disableSharedRelros() called"); + } + synchronized (mLock) { + ensureInitializedLocked(); + mInBrowserProcess = false; + mWaitForSharedRelros = false; + mBrowserUsesSharedRelro = false; + } + } + + /** + * Call this method before loading any libraries to indicate that this + * process is ready to reuse shared RELRO sections from another one. + * Typically used when starting service processes. + * + * @param baseLoadAddress the base library load address to use. + */ + public void initServiceProcess(long baseLoadAddress) { + if (DEBUG) { + Log.i(TAG, + String.format(Locale.US, "initServiceProcess(0x%x) called", baseLoadAddress)); + } + synchronized (mLock) { + ensureInitializedLocked(); + mInBrowserProcess = false; + mBrowserUsesSharedRelro = false; + mWaitForSharedRelros = true; + mBaseLoadAddress = baseLoadAddress; + mCurrentLoadAddress = baseLoadAddress; + } + } + + /** + * Retrieve the base load address of all shared RELRO sections. + * This also enforces the creation of shared RELRO sections in + * prepareLibraryLoad(), which can later be retrieved with getSharedRelros(). + * + * @return a common, random base load address, or 0 if RELRO sharing is + * disabled. + */ + public long getBaseLoadAddress() { + synchronized (mLock) { + ensureInitializedLocked(); + if (!mInBrowserProcess) { + Log.w(TAG, "Shared RELRO sections are disabled in this process!"); + return 0; + } + + setupBaseLoadAddressLocked(); + if (DEBUG) { + Log.i(TAG, + String.format( + Locale.US, "getBaseLoadAddress() returns 0x%x", mBaseLoadAddress)); + } + return mBaseLoadAddress; + } + } + + // Used internally to lazily setup the common random base load address. + private void setupBaseLoadAddressLocked() { + assert Thread.holdsLock(mLock); + if (mBaseLoadAddress == -1) { + mBaseLoadAddress = getRandomBaseLoadAddress(); + mCurrentLoadAddress = mBaseLoadAddress; + if (mBaseLoadAddress == 0) { + // If the random address is 0 there are issues with finding enough + // free address space, so disable RELRO shared / fixed load addresses. + Log.w(TAG, "Disabling shared RELROs due address space pressure"); + mBrowserUsesSharedRelro = false; + mWaitForSharedRelros = false; + } + } + } + + // Used for debugging only. + private void dumpBundle(Bundle bundle) { + if (DEBUG) { + Log.i(TAG, "Bundle has " + bundle.size() + " items: " + bundle); + } + } + + /** + * Use the shared RELRO section from a Bundle received form another process. + * Call this after calling setBaseLoadAddress() then loading all libraries + * with loadLibrary(). + * + * @param bundle Bundle instance generated with createSharedRelroBundle() in + * another process. + */ + private void useSharedRelrosLocked(Bundle bundle) { + assert Thread.holdsLock(mLock); + + if (DEBUG) { + Log.i(TAG, "Linker.useSharedRelrosLocked() called"); + } + + if (bundle == null) { + if (DEBUG) { + Log.i(TAG, "null bundle!"); + } + return; + } + + if (mLoadedLibraries == null) { + if (DEBUG) { + Log.i(TAG, "No libraries loaded!"); + } + return; + } + + if (DEBUG) { + dumpBundle(bundle); + } + HashMap<String, LibInfo> relroMap = createLibInfoMapFromBundle(bundle); + + // Apply the RELRO section to all libraries that were already loaded. + for (Map.Entry<String, LibInfo> entry : relroMap.entrySet()) { + String libName = entry.getKey(); + LibInfo libInfo = entry.getValue(); + if (!nativeUseSharedRelro(libName, libInfo)) { + Log.w(TAG, "Could not use shared RELRO section for " + libName); + } else { + if (DEBUG) { + Log.i(TAG, "Using shared RELRO section for " + libName); + } + } + } + + // In service processes, close all file descriptors from the map now. + if (!mInBrowserProcess) { + closeLibInfoMap(relroMap); + } + + if (DEBUG) { + Log.i(TAG, "Linker.useSharedRelrosLocked() exiting"); + } + } + + /** + * Implements loading a native shared library with the Chromium linker. + * + * Load a native shared library with the Chromium linker. If the zip file + * is not null, the shared library must be uncompressed and page aligned + * inside the zipfile. Note the crazy linker treats libraries and files as + * equivalent, so you can only open one library in a given zip file. The + * library must not be the Chromium linker library. + * + * @param libFilePath The path of the library (possibly in the zip file). + * @param isFixedAddressPermitted If true, uses a fixed load address if one was + * supplied, otherwise ignores the fixed address and loads wherever available. + */ + void loadLibraryImpl(String libFilePath, boolean isFixedAddressPermitted) { + if (DEBUG) { + Log.i(TAG, "loadLibraryImpl: " + libFilePath + ", " + isFixedAddressPermitted); + } + synchronized (mLock) { + ensureInitializedLocked(); + + // Security: Ensure prepareLibraryLoad() was called before. + // In theory, this can be done lazily here, but it's more consistent + // to use a pair of functions (i.e. prepareLibraryLoad() + finishLibraryLoad()) + // that wrap all calls to loadLibrary() in the library loader. + assert mPrepareLibraryLoadCalled; + + if (mLoadedLibraries == null) { + mLoadedLibraries = new HashMap<String, LibInfo>(); + } + + if (mLoadedLibraries.containsKey(libFilePath)) { + if (DEBUG) { + Log.i(TAG, "Not loading " + libFilePath + " twice"); + } + return; + } + + LibInfo libInfo = new LibInfo(); + long loadAddress = 0; + if (isFixedAddressPermitted) { + if ((mInBrowserProcess && mBrowserUsesSharedRelro) || mWaitForSharedRelros) { + // Load the library at a fixed address. + loadAddress = mCurrentLoadAddress; + + // For multiple libraries, ensure we stay within reservation range. + if (loadAddress > mBaseLoadAddress + ADDRESS_SPACE_RESERVATION) { + String errorMessage = + "Load address outside reservation, for: " + libFilePath; + Log.e(TAG, errorMessage); + throw new UnsatisfiedLinkError(errorMessage); + } + } + } + + final String sharedRelRoName = libFilePath; + if (!nativeLoadLibrary(libFilePath, loadAddress, libInfo)) { + String errorMessage = "Unable to load library: " + libFilePath; + Log.e(TAG, errorMessage); + throw new UnsatisfiedLinkError(errorMessage); + } + + // Print the load address to the logcat when testing the linker. The format + // of the string is expected by the Python test_runner script as one of: + // BROWSER_LIBRARY_ADDRESS: <library-name> <address> + // RENDERER_LIBRARY_ADDRESS: <library-name> <address> + // Where <library-name> is the library name, and <address> is the hexadecimal load + // address. + if (NativeLibraries.sEnableLinkerTests) { + String tag = + mInBrowserProcess ? "BROWSER_LIBRARY_ADDRESS" : "RENDERER_LIBRARY_ADDRESS"; + Log.i(TAG, + String.format( + Locale.US, "%s: %s %x", tag, libFilePath, libInfo.mLoadAddress)); + } + + if (mInBrowserProcess) { + // Create a new shared RELRO section at the 'current' fixed load address. + if (!nativeCreateSharedRelro(sharedRelRoName, mCurrentLoadAddress, libInfo)) { + Log.w(TAG, + String.format(Locale.US, "Could not create shared RELRO for %s at %x", + libFilePath, mCurrentLoadAddress)); + } else { + if (DEBUG) { + Log.i(TAG, + String.format(Locale.US, "Created shared RELRO for %s at %x: %s", + sharedRelRoName, mCurrentLoadAddress, libInfo.toString())); + } + } + } + + if (loadAddress != 0 && mCurrentLoadAddress != 0) { + // Compute the next current load address. If mCurrentLoadAddress + // is not 0, this is an explicit library load address. Otherwise, + // this is an explicit load address for relocated RELRO sections + // only. + mCurrentLoadAddress = + libInfo.mLoadAddress + libInfo.mLoadSize + BREAKPAD_GUARD_REGION_BYTES; + } + + mLoadedLibraries.put(sharedRelRoName, libInfo); + if (DEBUG) { + Log.i(TAG, "Library details " + libInfo.toString()); + } + } + } + + /** + * Record information for a given library. + * IMPORTANT: Native code knows about this class's fields, so + * don't change them without modifying the corresponding C++ sources. + * Also, the LibInfo instance owns the shared RELRO file descriptor. + */ + private static class LibInfo implements Parcelable { + LibInfo() {} + + // from Parcelable + LibInfo(Parcel in) { + mLoadAddress = in.readLong(); + mLoadSize = in.readLong(); + mRelroStart = in.readLong(); + mRelroSize = in.readLong(); + ParcelFileDescriptor fd = ParcelFileDescriptor.CREATOR.createFromParcel(in); + // If CreateSharedRelro fails, the OS file descriptor will be -1 and |fd| will be null. + if (fd != null) { + mRelroFd = fd.detachFd(); + } + } + + public void close() { + if (mRelroFd >= 0) { + StreamUtil.closeQuietly(ParcelFileDescriptor.adoptFd(mRelroFd)); + mRelroFd = -1; + } + } + + // from Parcelable + @Override + public void writeToParcel(Parcel out, int flags) { + if (mRelroFd >= 0) { + out.writeLong(mLoadAddress); + out.writeLong(mLoadSize); + out.writeLong(mRelroStart); + out.writeLong(mRelroSize); + try { + ParcelFileDescriptor fd = ParcelFileDescriptor.fromFd(mRelroFd); + fd.writeToParcel(out, 0); + fd.close(); + } catch (java.io.IOException e) { + Log.e(TAG, "Can't write LibInfo file descriptor to parcel", e); + } + } + } + + // from Parcelable + @Override + public int describeContents() { + return Parcelable.CONTENTS_FILE_DESCRIPTOR; + } + + // from Parcelable + public static final Parcelable.Creator<LibInfo> CREATOR = + new Parcelable.Creator<LibInfo>() { + @Override + public LibInfo createFromParcel(Parcel in) { + return new LibInfo(in); + } + + @Override + public LibInfo[] newArray(int size) { + return new LibInfo[size]; + } + }; + + // IMPORTANT: Don't change these fields without modifying the + // native code that accesses them directly! + @AccessedByNative + public long mLoadAddress; // page-aligned library load address. + @AccessedByNative + public long mLoadSize; // page-aligned library load size. + @AccessedByNative + public long mRelroStart; // page-aligned address in memory, or 0 if none. + @AccessedByNative + public long mRelroSize; // page-aligned size in memory, or 0. + @AccessedByNative + public int mRelroFd = -1; // shared RELRO file descriptor, or -1 + } + + // Create a Bundle from a map of LibInfo objects. + private Bundle createBundleFromLibInfoMap(HashMap<String, LibInfo> map) { + Bundle bundle = new Bundle(map.size()); + for (Map.Entry<String, LibInfo> entry : map.entrySet()) { + bundle.putParcelable(entry.getKey(), entry.getValue()); + } + return bundle; + } + + // Create a new LibInfo map from a Bundle. + private HashMap<String, LibInfo> createLibInfoMapFromBundle(Bundle bundle) { + HashMap<String, LibInfo> map = new HashMap<String, LibInfo>(); + for (String library : bundle.keySet()) { + LibInfo libInfo = bundle.getParcelable(library); + map.put(library, libInfo); + } + return map; + } + + // Call the close() method on all values of a LibInfo map. + private void closeLibInfoMap(HashMap<String, LibInfo> map) { + for (Map.Entry<String, LibInfo> entry : map.entrySet()) { + entry.getValue().close(); + } + } + + /** + * Move activity from the native thread to the main UI thread. + * Called from native code on its own thread. Posts a callback from + * the UI thread back to native code. + * + * @param opaque Opaque argument. + */ + @CalledByNative + @MainDex + private static void postCallbackOnMainThread(final long opaque) { + ThreadUtils.postOnUiThread(new Runnable() { + @Override + public void run() { + nativeRunCallbackOnUiThread(opaque); + } + }); + } + + /** + * Native method to run callbacks on the main UI thread. + * Supplied by the crazy linker and called by postCallbackOnMainThread. + * + * @param opaque Opaque crazy linker arguments. + */ + private static native void nativeRunCallbackOnUiThread(long opaque); + + /** + * Native method used to load a library. + * + * @param library Platform specific library name (e.g. libfoo.so) + * @param loadAddress Explicit load address, or 0 for randomized one. + * @param libInfo If not null, the mLoadAddress and mLoadSize fields + * of this LibInfo instance will set on success. + * @return true for success, false otherwise. + */ + private static native boolean nativeLoadLibrary( + String library, long loadAddress, LibInfo libInfo); + + /** + * Native method used to add a zip archive or APK to the search path + * for native libraries. Allows loading directly from it. + * + * @param zipfilePath Path of the zip file containing the libraries. + * @return true for success, false otherwise. + */ + private static native boolean nativeAddZipArchivePath(String zipFilePath); + + /** + * Native method used to create a shared RELRO section. + * If the library was already loaded at the same address using + * nativeLoadLibrary(), this creates the RELRO for it. Otherwise, + * this loads a new temporary library at the specified address, + * creates and extracts the RELRO section from it, then unloads it. + * + * @param library Library name. + * @param loadAddress load address, which can be different from the one + * used to load the library in the current process! + * @param libInfo libInfo instance. On success, the mRelroStart, mRelroSize + * and mRelroFd will be set. + * @return true on success, false otherwise. + */ + private static native boolean nativeCreateSharedRelro( + String library, long loadAddress, LibInfo libInfo); + + /** + * Native method used to use a shared RELRO section. + * + * @param library Library name. + * @param libInfo A LibInfo instance containing valid RELRO information + * @return true on success. + */ + private static native boolean nativeUseSharedRelro(String library, LibInfo libInfo); + + /** + * Return a random address that should be free to be mapped with the given size. + * Maps an area large enough for the largest library we might attempt to load, + * and if successful then unmaps it and returns the address of the area allocated + * by the system (with ASLR). The idea is that this area should remain free of + * other mappings until we map our library into it. + * + * @return address to pass to future mmap, or 0 on error. + */ + private static native long nativeGetRandomBaseLoadAddress(); +} diff --git a/base/android/java/src/org/chromium/base/library_loader/LoaderErrors.java b/base/android/java/src/org/chromium/base/library_loader/LoaderErrors.java new file mode 100644 index 0000000000..2b94370bd8 --- /dev/null +++ b/base/android/java/src/org/chromium/base/library_loader/LoaderErrors.java @@ -0,0 +1,16 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.library_loader; + +/** + * These are the possible failures from the LibraryLoader + */ +public class LoaderErrors { + public static final int LOADER_ERROR_NORMAL_COMPLETION = 0; + public static final int LOADER_ERROR_FAILED_TO_REGISTER_JNI = 1; + public static final int LOADER_ERROR_NATIVE_LIBRARY_LOAD_FAILED = 2; + public static final int LOADER_ERROR_NATIVE_LIBRARY_WRONG_VERSION = 3; + public static final int LOADER_ERROR_NATIVE_STARTUP_FAILED = 4; +} diff --git a/base/android/java/src/org/chromium/base/library_loader/NativeLibraryPreloader.java b/base/android/java/src/org/chromium/base/library_loader/NativeLibraryPreloader.java new file mode 100644 index 0000000000..6f8008d645 --- /dev/null +++ b/base/android/java/src/org/chromium/base/library_loader/NativeLibraryPreloader.java @@ -0,0 +1,20 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.library_loader; + +import android.content.Context; + +/** + * This is interface to preload the native library before calling System.loadLibrary. + * + * Preloading shouldn't call System.loadLibrary() or otherwise cause any Chromium + * code to be run, because it can be called before Chromium command line is known. + * It can however open the library via dlopen() or android_dlopen_ext() so that + * dlopen() later called by System.loadLibrary() becomes a noop. This is what the + * only subclass (MonochromeLibraryPreloader) is doing. + */ +public abstract class NativeLibraryPreloader { + public abstract int loadLibrary(Context context); +} diff --git a/base/android/java/src/org/chromium/base/library_loader/ProcessInitException.java b/base/android/java/src/org/chromium/base/library_loader/ProcessInitException.java new file mode 100644 index 0000000000..106667536d --- /dev/null +++ b/base/android/java/src/org/chromium/base/library_loader/ProcessInitException.java @@ -0,0 +1,35 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.library_loader; + +/** + * The exception that is thrown when the intialization of a process was failed. + */ +public class ProcessInitException extends Exception { + private int mErrorCode = LoaderErrors.LOADER_ERROR_NORMAL_COMPLETION; + + /** + * @param errorCode This will be one of the LoaderErrors error codes. + */ + public ProcessInitException(int errorCode) { + mErrorCode = errorCode; + } + + /** + * @param errorCode This will be one of the LoaderErrors error codes. + * @param throwable The wrapped throwable obj. + */ + public ProcessInitException(int errorCode, Throwable throwable) { + super(null, throwable); + mErrorCode = errorCode; + } + + /** + * Return the error code. + */ + public int getErrorCode() { + return mErrorCode; + } +} diff --git a/base/android/java/src/org/chromium/base/memory/MemoryPressureCallback.java b/base/android/java/src/org/chromium/base/memory/MemoryPressureCallback.java new file mode 100644 index 0000000000..258aa0bbdf --- /dev/null +++ b/base/android/java/src/org/chromium/base/memory/MemoryPressureCallback.java @@ -0,0 +1,15 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.memory; + +import org.chromium.base.MemoryPressureLevel; + +/** + * Memory pressure callback interface. + */ +@FunctionalInterface +public interface MemoryPressureCallback { + public void onPressure(@MemoryPressureLevel int pressure); +} diff --git a/base/android/java/src/org/chromium/base/memory/MemoryPressureMonitor.java b/base/android/java/src/org/chromium/base/memory/MemoryPressureMonitor.java new file mode 100644 index 0000000000..c8af484682 --- /dev/null +++ b/base/android/java/src/org/chromium/base/memory/MemoryPressureMonitor.java @@ -0,0 +1,301 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.memory; + +import android.app.ActivityManager; +import android.content.ComponentCallbacks2; +import android.content.res.Configuration; +import android.os.Build; +import android.os.SystemClock; + +import org.chromium.base.ContextUtils; +import org.chromium.base.MemoryPressureLevel; +import org.chromium.base.MemoryPressureListener; +import org.chromium.base.Supplier; +import org.chromium.base.ThreadUtils; +import org.chromium.base.VisibleForTesting; +import org.chromium.base.annotations.MainDex; +import org.chromium.base.metrics.CachedMetrics; + +import java.util.concurrent.TimeUnit; + +/** + * This class monitors memory pressure and reports it to the native side. + * Even though there can be other callbacks besides MemoryPressureListener (which reports + * pressure to the native side, and is added implicitly), the class is designed to suite + * needs of native MemoryPressureListeners. + * + * There are two groups of MemoryPressureListeners: + * + * 1. Stateless, i.e. ones that simply free memory (caches, etc.) in response to memory + * pressure. These listeners need to be called periodically (to have effect), but not + * too frequently (to avoid regressing performance too much). + * + * 2. Stateful, i.e. ones that change their behavior based on the last received memory + * pressure (in addition to freeing memory). These listeners need to know when the + * pressure subsides, i.e. they need to be notified about CRITICAL->MODERATE changes. + * + * Android notifies about memory pressure through onTrimMemory() / onLowMemory() callbacks + * from ComponentCallbacks2, but these are unreliable (e.g. called too early, called just + * once, not called when memory pressure subsides, etc., see https://crbug.com/813909 for + * more examples). + * + * There is also ActivityManager.getMyMemoryState() API which returns current pressure for + * the calling process. It has its caveats, for example it can't be called from isolated + * processes (renderers). Plus we don't want to poll getMyMemoryState() unnecessarily, for + * example there is no reason to poll it when Chrome is in the background. + * + * This class implements the following principles: + * + * 1. Throttle pressure signals sent to callbacks. + * Callbacks are called at most once during throttling interval. If same pressure is + * reported several times during the interval, all reports except the first one are + * ignored. + * + * 2. Always report changes in pressure. + * If pressure changes during the interval, the change is not ignored, but delayed + * until the end of the interval. + * + * 3. Poll on CRITICAL memory pressure. + * Once CRITICAL pressure is reported, getMyMemoryState API is used to periodically + * query pressure until it subsides (becomes non-CRITICAL). + * + * Zooming out, the class is used as follows: + * + * 1. Only the browser process / WebView process poll, and it only polls when it makes + * sense to do so (when Chrome is in the foreground / there are WebView instances + * around). + * + * 2. Services (GPU, renderers) don't poll, instead they get additional pressure signals + * from the main process. + * + * NOTE: This class should only be used on UiThread as defined by ThreadUtils (which is + * Android main thread for Chrome, but can be some other thread for WebView). + */ +@MainDex +public class MemoryPressureMonitor { + private static final int DEFAULT_THROTTLING_INTERVAL_MS = 60 * 1000; + + private final int mThrottlingIntervalMs; + + // Pressure reported to callbacks in the current throttling interval. + private @MemoryPressureLevel int mLastReportedPressure = MemoryPressureLevel.NONE; + + // Pressure received (but not reported) during the current throttling interval, + // or null if no pressure was received. + private @MemoryPressureLevel Integer mThrottledPressure; + + // Whether we need to throttle pressure signals. + private boolean mIsInsideThrottlingInterval; + + private boolean mPollingEnabled; + + // Changed by tests. + private Supplier<Integer> mCurrentPressureSupplier = + MemoryPressureMonitor::getCurrentMemoryPressure; + + // Changed by tests. + private MemoryPressureCallback mReportingCallback = + MemoryPressureListener::notifyMemoryPressure; + + private final Runnable mThrottlingIntervalTask = this ::onThrottlingIntervalFinished; + + // ActivityManager.getMyMemoryState() time histograms, recorded by getCurrentMemoryPressure(). + // Using Count1MHistogramSample because TimesHistogramSample doesn't support microsecond + // precision. + private static final CachedMetrics.Count1MHistogramSample sGetMyMemoryStateSucceededTime = + new CachedMetrics.Count1MHistogramSample( + "Android.MemoryPressureMonitor.GetMyMemoryState.Succeeded.Time"); + private static final CachedMetrics.Count1MHistogramSample sGetMyMemoryStateFailedTime = + new CachedMetrics.Count1MHistogramSample( + "Android.MemoryPressureMonitor.GetMyMemoryState.Failed.Time"); + + // The only instance. + public static final MemoryPressureMonitor INSTANCE = + new MemoryPressureMonitor(DEFAULT_THROTTLING_INTERVAL_MS); + + @VisibleForTesting + protected MemoryPressureMonitor(int throttlingIntervalMs) { + mThrottlingIntervalMs = throttlingIntervalMs; + } + + /** + * Starts listening to ComponentCallbacks2. + */ + public void registerComponentCallbacks() { + ThreadUtils.assertOnUiThread(); + + ContextUtils.getApplicationContext().registerComponentCallbacks(new ComponentCallbacks2() { + @Override + public void onTrimMemory(int level) { + Integer pressure = memoryPressureFromTrimLevel(level); + if (pressure != null) { + notifyPressure(pressure); + } + } + + @Override + public void onLowMemory() { + notifyPressure(MemoryPressureLevel.CRITICAL); + } + + @Override + public void onConfigurationChanged(Configuration configuration) {} + }); + } + + /** + * Enables memory pressure polling. + * See class comment for specifics. This method also does a single pressure check to get + * the current pressure. + */ + public void enablePolling() { + ThreadUtils.assertOnUiThread(); + if (mPollingEnabled) return; + + mPollingEnabled = true; + if (!mIsInsideThrottlingInterval) { + reportCurrentPressure(); + } + } + + /** + * Disables memory pressure polling. + */ + public void disablePolling() { + ThreadUtils.assertOnUiThread(); + if (!mPollingEnabled) return; + + mPollingEnabled = false; + } + + /** + * Notifies the class about change in memory pressure. + * Note that |pressure| might get throttled or delayed, i.e. calling this method doesn't + * necessarily call the callbacks. See the class comment. + */ + public void notifyPressure(@MemoryPressureLevel int pressure) { + ThreadUtils.assertOnUiThread(); + + if (mIsInsideThrottlingInterval) { + // We've already reported during this interval. Save |pressure| and act on + // it later, when the interval finishes. + mThrottledPressure = pressure; + return; + } + + reportPressure(pressure); + } + + /** + * Last pressure that was reported to MemoryPressureListener. + * Returns MemoryPressureLevel.NONE if nothing was reported yet. + */ + public @MemoryPressureLevel int getLastReportedPressure() { + ThreadUtils.assertOnUiThread(); + return mLastReportedPressure; + } + + private void reportPressure(@MemoryPressureLevel int pressure) { + assert !mIsInsideThrottlingInterval : "Can't report pressure when throttling."; + + startThrottlingInterval(); + + mLastReportedPressure = pressure; + mReportingCallback.onPressure(pressure); + } + + private void onThrottlingIntervalFinished() { + mIsInsideThrottlingInterval = false; + + // If there was a pressure change during the interval, report it. + if (mThrottledPressure != null && mLastReportedPressure != mThrottledPressure) { + int throttledPressure = mThrottledPressure; + mThrottledPressure = null; + reportPressure(throttledPressure); + return; + } + + // The pressure didn't change during the interval. Report current pressure + // (starting a new interval) if we need to. + if (mPollingEnabled && mLastReportedPressure == MemoryPressureLevel.CRITICAL) { + reportCurrentPressure(); + } + } + + private void reportCurrentPressure() { + Integer pressure = mCurrentPressureSupplier.get(); + if (pressure != null) { + reportPressure(pressure); + } + } + + private void startThrottlingInterval() { + ThreadUtils.postOnUiThreadDelayed(mThrottlingIntervalTask, mThrottlingIntervalMs); + mIsInsideThrottlingInterval = true; + } + + @VisibleForTesting + public void setCurrentPressureSupplierForTesting(Supplier<Integer> supplier) { + mCurrentPressureSupplier = supplier; + } + + @VisibleForTesting + public void setReportingCallbackForTesting(MemoryPressureCallback callback) { + mReportingCallback = callback; + } + + /** + * Queries current memory pressure. + * Returns null if the pressure couldn't be determined. + */ + private static @MemoryPressureLevel Integer getCurrentMemoryPressure() { + long startNanos = elapsedRealtimeNanos(); + try { + ActivityManager.RunningAppProcessInfo processInfo = + new ActivityManager.RunningAppProcessInfo(); + ActivityManager.getMyMemoryState(processInfo); + recordRealtimeNanosDuration(sGetMyMemoryStateSucceededTime, startNanos); + return memoryPressureFromTrimLevel(processInfo.lastTrimLevel); + } catch (Exception e) { + // Defensively catch all exceptions, just in case. + recordRealtimeNanosDuration(sGetMyMemoryStateFailedTime, startNanos); + return null; + } + } + + private static void recordRealtimeNanosDuration( + CachedMetrics.Count1MHistogramSample histogram, long startNanos) { + // We're using Count1MHistogram, so we need to calculate duration in microseconds + long durationUs = TimeUnit.NANOSECONDS.toMicros(elapsedRealtimeNanos() - startNanos); + // record() takes int, so we need to clamp. + histogram.record((int) Math.min(durationUs, Integer.MAX_VALUE)); + } + + private static long elapsedRealtimeNanos() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return SystemClock.elapsedRealtimeNanos(); + } else { + return SystemClock.elapsedRealtime() * 1000000; + } + } + + /** + * Maps ComponentCallbacks2.TRIM_* value to MemoryPressureLevel. + * Returns null if |level| couldn't be mapped and should be ignored. + */ + @VisibleForTesting + public static @MemoryPressureLevel Integer memoryPressureFromTrimLevel(int level) { + if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE + || level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { + return MemoryPressureLevel.CRITICAL; + } else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { + // Don't notify on TRIM_MEMORY_UI_HIDDEN, since this class only + // dispatches actionable memory pressure signals to native. + return MemoryPressureLevel.MODERATE; + } + return null; + } +} diff --git a/base/android/java/src/org/chromium/base/memory/MemoryPressureUma.java b/base/android/java/src/org/chromium/base/memory/MemoryPressureUma.java new file mode 100644 index 0000000000..dc90f5706e --- /dev/null +++ b/base/android/java/src/org/chromium/base/memory/MemoryPressureUma.java @@ -0,0 +1,113 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.memory; + +import android.content.ComponentCallbacks2; +import android.content.res.Configuration; +import android.support.annotation.IntDef; + +import org.chromium.base.ContextUtils; +import org.chromium.base.ThreadUtils; +import org.chromium.base.metrics.RecordHistogram; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Centralizes UMA data collection for Android-specific memory conditions. + */ +public class MemoryPressureUma implements ComponentCallbacks2 { + @IntDef({ + Notification.UNKNOWN_TRIM_LEVEL, Notification.TRIM_MEMORY_COMPLETE, + Notification.TRIM_MEMORY_MODERATE, Notification.TRIM_MEMORY_BACKGROUND, + Notification.TRIM_MEMORY_UI_HIDDEN, Notification.TRIM_MEMORY_RUNNING_CRITICAL, + Notification.TRIM_MEMORY_RUNNING_LOW, Notification.TRIM_MEMORY_RUNNING_MODERATE, + Notification.ON_LOW_MEMORY, Notification.NOTIFICATION_MAX, + }) + @Retention(RetentionPolicy.SOURCE) + private @interface Notification { + // WARNING: These values are persisted to logs. Entries should not be + // renumbered and numeric values should never be reused. + // Keep in sync with "Android.MemoryPressureNotification" UMA enum. + int UNKNOWN_TRIM_LEVEL = 0; + int TRIM_MEMORY_COMPLETE = 1; + int TRIM_MEMORY_MODERATE = 2; + int TRIM_MEMORY_BACKGROUND = 3; + int TRIM_MEMORY_UI_HIDDEN = 4; + int TRIM_MEMORY_RUNNING_CRITICAL = 5; + int TRIM_MEMORY_RUNNING_LOW = 6; + int TRIM_MEMORY_RUNNING_MODERATE = 7; + int ON_LOW_MEMORY = 8; + + // Must be the last one. + int NOTIFICATION_MAX = 9; + } + + private final String mHistogramName; + + private static MemoryPressureUma sInstance; + + public static void initializeForBrowser() { + initializeInstance("Browser"); + } + + public static void initializeForChildService() { + initializeInstance("ChildService"); + } + + private static void initializeInstance(String processType) { + ThreadUtils.assertOnUiThread(); + assert sInstance == null; + sInstance = new MemoryPressureUma(processType); + ContextUtils.getApplicationContext().registerComponentCallbacks(sInstance); + } + + private MemoryPressureUma(String processType) { + mHistogramName = "Android.MemoryPressureNotification." + processType; + } + + @Override + public void onLowMemory() { + record(Notification.ON_LOW_MEMORY); + } + + @Override + public void onTrimMemory(int level) { + switch (level) { + case TRIM_MEMORY_COMPLETE: + record(Notification.TRIM_MEMORY_COMPLETE); + break; + case TRIM_MEMORY_MODERATE: + record(Notification.TRIM_MEMORY_MODERATE); + break; + case TRIM_MEMORY_BACKGROUND: + record(Notification.TRIM_MEMORY_BACKGROUND); + break; + case TRIM_MEMORY_UI_HIDDEN: + record(Notification.TRIM_MEMORY_UI_HIDDEN); + break; + case TRIM_MEMORY_RUNNING_CRITICAL: + record(Notification.TRIM_MEMORY_RUNNING_CRITICAL); + break; + case TRIM_MEMORY_RUNNING_LOW: + record(Notification.TRIM_MEMORY_RUNNING_LOW); + break; + case TRIM_MEMORY_RUNNING_MODERATE: + record(Notification.TRIM_MEMORY_RUNNING_MODERATE); + break; + default: + record(Notification.UNKNOWN_TRIM_LEVEL); + break; + } + } + + @Override + public void onConfigurationChanged(Configuration configuration) {} + + private void record(@Notification int notification) { + RecordHistogram.recordEnumeratedHistogram( + mHistogramName, notification, Notification.NOTIFICATION_MAX); + } +} diff --git a/base/android/java/src/org/chromium/base/metrics/CachedMetrics.java b/base/android/java/src/org/chromium/base/metrics/CachedMetrics.java new file mode 100644 index 0000000000..ba03e51275 --- /dev/null +++ b/base/android/java/src/org/chromium/base/metrics/CachedMetrics.java @@ -0,0 +1,307 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.metrics; + +import org.chromium.base.library_loader.LibraryLoader; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Utility classes for recording UMA metrics before the native library + * may have been loaded. Metrics are cached until the library is known + * to be loaded, then committed to the MetricsService all at once. + */ +public class CachedMetrics { + /** + * Base class for cached metric objects. Subclasses are expected to call + * addToCache() when some metric state gets recorded that requires a later + * commit operation when the native library is loaded. + */ + private abstract static class CachedMetric { + private static final List<CachedMetric> sMetrics = new ArrayList<CachedMetric>(); + + protected final String mName; + protected boolean mCached; + + /** + * @param name Name of the metric to record. + */ + protected CachedMetric(String name) { + mName = name; + } + + /** + * Adds this object to the sMetrics cache, if it hasn't been added already. + * Must be called while holding the synchronized(sMetrics) lock. + * Note: The synchronization is not done inside this function because subclasses + * need to increment their held values under lock to ensure thread-safety. + */ + protected final void addToCache() { + assert Thread.holdsLock(sMetrics); + + if (mCached) return; + sMetrics.add(this); + mCached = true; + } + + /** + * Commits the metric. Expects the native library to be loaded. + * Must be called while holding the synchronized(sMetrics) lock. + */ + protected abstract void commitAndClear(); + } + + /** + * Caches an action that will be recorded after native side is loaded. + */ + public static class ActionEvent extends CachedMetric { + private int mCount; + + public ActionEvent(String actionName) { + super(actionName); + } + + public void record() { + synchronized (CachedMetric.sMetrics) { + if (LibraryLoader.getInstance().isInitialized()) { + recordWithNative(); + } else { + mCount++; + addToCache(); + } + } + } + + private void recordWithNative() { + RecordUserAction.record(mName); + } + + @Override + protected void commitAndClear() { + while (mCount > 0) { + recordWithNative(); + mCount--; + } + } + } + + /** Caches a set of integer histogram samples. */ + public static class SparseHistogramSample extends CachedMetric { + private final List<Integer> mSamples = new ArrayList<Integer>(); + + public SparseHistogramSample(String histogramName) { + super(histogramName); + } + + public void record(int sample) { + synchronized (CachedMetric.sMetrics) { + if (LibraryLoader.getInstance().isInitialized()) { + recordWithNative(sample); + } else { + mSamples.add(sample); + addToCache(); + } + } + } + + private void recordWithNative(int sample) { + RecordHistogram.recordSparseSlowlyHistogram(mName, sample); + } + + @Override + protected void commitAndClear() { + for (Integer sample : mSamples) { + recordWithNative(sample); + } + mSamples.clear(); + } + } + + /** Caches a set of enumerated histogram samples. */ + public static class EnumeratedHistogramSample extends CachedMetric { + private final List<Integer> mSamples = new ArrayList<Integer>(); + private final int mMaxValue; + + public EnumeratedHistogramSample(String histogramName, int maxValue) { + super(histogramName); + mMaxValue = maxValue; + } + + public void record(int sample) { + synchronized (CachedMetric.sMetrics) { + if (LibraryLoader.getInstance().isInitialized()) { + recordWithNative(sample); + } else { + mSamples.add(sample); + addToCache(); + } + } + } + + private void recordWithNative(int sample) { + RecordHistogram.recordEnumeratedHistogram(mName, sample, mMaxValue); + } + + @Override + protected void commitAndClear() { + for (Integer sample : mSamples) { + recordWithNative(sample); + } + mSamples.clear(); + } + } + + /** Caches a set of times histogram samples. */ + public static class TimesHistogramSample extends CachedMetric { + private final List<Long> mSamples = new ArrayList<Long>(); + private final TimeUnit mTimeUnit; + + public TimesHistogramSample(String histogramName, TimeUnit timeUnit) { + super(histogramName); + RecordHistogram.assertTimesHistogramSupportsUnit(timeUnit); + mTimeUnit = timeUnit; + } + + public void record(long sample) { + synchronized (CachedMetric.sMetrics) { + if (LibraryLoader.getInstance().isInitialized()) { + recordWithNative(sample); + } else { + mSamples.add(sample); + addToCache(); + } + } + } + + private void recordWithNative(long sample) { + RecordHistogram.recordTimesHistogram(mName, sample, mTimeUnit); + } + + @Override + protected void commitAndClear() { + for (Long sample : mSamples) { + recordWithNative(sample); + } + mSamples.clear(); + } + } + + /** Caches a set of boolean histogram samples. */ + public static class BooleanHistogramSample extends CachedMetric { + private final List<Boolean> mSamples = new ArrayList<Boolean>(); + + public BooleanHistogramSample(String histogramName) { + super(histogramName); + } + + public void record(boolean sample) { + synchronized (CachedMetric.sMetrics) { + if (LibraryLoader.getInstance().isInitialized()) { + recordWithNative(sample); + } else { + mSamples.add(sample); + addToCache(); + } + } + } + + private void recordWithNative(boolean sample) { + RecordHistogram.recordBooleanHistogram(mName, sample); + } + + @Override + protected void commitAndClear() { + for (Boolean sample : mSamples) { + recordWithNative(sample); + } + mSamples.clear(); + } + } + + /** + * Caches a set of custom count histogram samples. + * Corresponds to UMA_HISTOGRAM_CUSTOM_COUNTS C++ macro. + */ + public static class CustomCountHistogramSample extends CachedMetric { + private final List<Integer> mSamples = new ArrayList<Integer>(); + private final int mMin; + private final int mMax; + private final int mNumBuckets; + + public CustomCountHistogramSample(String histogramName, int min, int max, int numBuckets) { + super(histogramName); + mMin = min; + mMax = max; + mNumBuckets = numBuckets; + } + + public void record(int sample) { + synchronized (CachedMetric.sMetrics) { + if (LibraryLoader.getInstance().isInitialized()) { + recordWithNative(sample); + } else { + mSamples.add(sample); + addToCache(); + } + } + } + + private void recordWithNative(int sample) { + RecordHistogram.recordCustomCountHistogram(mName, sample, mMin, mMax, mNumBuckets); + } + + @Override + protected void commitAndClear() { + for (Integer sample : mSamples) { + recordWithNative(sample); + } + mSamples.clear(); + } + } + + /** + * Caches a set of count histogram samples in range [1, 100). + * Corresponds to UMA_HISTOGRAM_COUNTS_100 C++ macro. + */ + public static class Count100HistogramSample extends CustomCountHistogramSample { + public Count100HistogramSample(String histogramName) { + super(histogramName, 1, 100, 50); + } + } + + /** + * Caches a set of count histogram samples in range [1, 1000). + * Corresponds to UMA_HISTOGRAM_COUNTS_1000 C++ macro. + */ + public static class Count1000HistogramSample extends CustomCountHistogramSample { + public Count1000HistogramSample(String histogramName) { + super(histogramName, 1, 1000, 50); + } + } + + /** + * Caches a set of count histogram samples in range [1, 1000000). + * Corresponds to UMA_HISTOGRAM_COUNTS_1M C++ macro. + */ + public static class Count1MHistogramSample extends CustomCountHistogramSample { + public Count1MHistogramSample(String histogramName) { + super(histogramName, 1, 1000000, 50); + } + } + + /** + * Calls out to native code to commit any cached histograms and events. + * Should be called once the native library has been loaded. + */ + public static void commitCachedMetrics() { + synchronized (CachedMetric.sMetrics) { + for (CachedMetric metric : CachedMetric.sMetrics) { + metric.commitAndClear(); + } + } + } +} diff --git a/base/android/java/src/org/chromium/base/metrics/RecordHistogram.java b/base/android/java/src/org/chromium/base/metrics/RecordHistogram.java new file mode 100644 index 0000000000..898f0094ab --- /dev/null +++ b/base/android/java/src/org/chromium/base/metrics/RecordHistogram.java @@ -0,0 +1,331 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.metrics; + +import org.chromium.base.VisibleForTesting; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.MainDex; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Java API for recording UMA histograms. + * + * Internally, histograms objects are cached on the Java side by their pointer + * values (converted to long). This is safe to do because C++ Histogram objects + * are never freed. Caching them on the Java side prevents needing to do costly + * Java String to C++ string conversions on the C++ side during lookup. + * + * Note: the JNI calls are relatively costly - avoid calling these methods in performance-critical + * code. + */ +@JNINamespace("base::android") +@MainDex +public class RecordHistogram { + private static Throwable sDisabledBy; + private static Map<String, Long> sCache = + Collections.synchronizedMap(new HashMap<String, Long>()); + + /** + * Tests may not have native initialized, so they may need to disable metrics. The value should + * be reset after the test done, to avoid carrying over state to unrelated tests. + * + * In JUnit tests this can be done automatically using + * {@link org.chromium.chrome.browser.DisableHistogramsRule} + */ + @VisibleForTesting + public static void setDisabledForTests(boolean disabled) { + if (disabled && sDisabledBy != null) { + throw new IllegalStateException("Histograms are already disabled.", sDisabledBy); + } + sDisabledBy = disabled ? new Throwable() : null; + } + + private static long getCachedHistogramKey(String name) { + Long key = sCache.get(name); + // Note: If key is null, we don't have it cached. In that case, pass 0 + // to the native code, which gets converted to a null histogram pointer + // which will cause the native code to look up the object on the native + // side. + return (key == null ? 0 : key); + } + + /** + * Records a sample in a boolean UMA histogram of the given name. Boolean histogram has two + * buckets, corresponding to success (true) and failure (false). This is the Java equivalent of + * the UMA_HISTOGRAM_BOOLEAN C++ macro. + * @param name name of the histogram + * @param sample sample to be recorded, either true or false + */ + public static void recordBooleanHistogram(String name, boolean sample) { + if (sDisabledBy != null) return; + long key = getCachedHistogramKey(name); + long result = nativeRecordBooleanHistogram(name, key, sample); + if (result != key) sCache.put(name, result); + } + + /** + * Records a sample in an enumerated histogram of the given name and boundary. Note that + * |boundary| identifies the histogram - it should be the same at every invocation. This is the + * Java equivalent of the UMA_HISTOGRAM_ENUMERATION C++ macro. + * @param name name of the histogram + * @param sample sample to be recorded, at least 0 and at most |boundary| - 1 + * @param boundary upper bound for legal sample values - all sample values have to be strictly + * lower than |boundary| + */ + public static void recordEnumeratedHistogram(String name, int sample, int boundary) { + if (sDisabledBy != null) return; + long key = getCachedHistogramKey(name); + long result = nativeRecordEnumeratedHistogram(name, key, sample, boundary); + if (result != key) sCache.put(name, result); + } + + /** + * Records a sample in a count histogram. This is the Java equivalent of the + * UMA_HISTOGRAM_COUNTS C++ macro. + * @param name name of the histogram + * @param sample sample to be recorded, at least 1 and at most 999999 + */ + public static void recordCountHistogram(String name, int sample) { + recordCustomCountHistogram(name, sample, 1, 1000000, 50); + } + + /** + * Records a sample in a count histogram. This is the Java equivalent of the + * UMA_HISTOGRAM_COUNTS_100 C++ macro. + * @param name name of the histogram + * @param sample sample to be recorded, at least 1 and at most 99 + */ + public static void recordCount100Histogram(String name, int sample) { + recordCustomCountHistogram(name, sample, 1, 100, 50); + } + + /** + * Records a sample in a count histogram. This is the Java equivalent of the + * UMA_HISTOGRAM_COUNTS_1000 C++ macro. + * @param name name of the histogram + * @param sample sample to be recorded, at least 1 and at most 999 + */ + public static void recordCount1000Histogram(String name, int sample) { + recordCustomCountHistogram(name, sample, 1, 1000, 50); + } + + /** + * Records a sample in a count histogram. This is the Java equivalent of the + * UMA_HISTOGRAM_CUSTOM_COUNTS C++ macro. + * @param name name of the histogram + * @param sample sample to be recorded, at least |min| and at most |max| - 1 + * @param min lower bound for expected sample values. It must be >= 1 + * @param max upper bounds for expected sample values + * @param numBuckets the number of buckets + */ + public static void recordCustomCountHistogram( + String name, int sample, int min, int max, int numBuckets) { + if (sDisabledBy != null) return; + long key = getCachedHistogramKey(name); + long result = nativeRecordCustomCountHistogram(name, key, sample, min, max, numBuckets); + if (result != key) sCache.put(name, result); + } + + /** + * Records a sample in a linear histogram. This is the Java equivalent for using + * base::LinearHistogram. + * @param name name of the histogram + * @param sample sample to be recorded, at least |min| and at most |max| - 1. + * @param min lower bound for expected sample values, should be at least 1. + * @param max upper bounds for expected sample values + * @param numBuckets the number of buckets + */ + public static void recordLinearCountHistogram( + String name, int sample, int min, int max, int numBuckets) { + if (sDisabledBy != null) return; + long key = getCachedHistogramKey(name); + long result = nativeRecordLinearCountHistogram(name, key, sample, min, max, numBuckets); + if (result != key) sCache.put(name, result); + } + + /** + * Records a sample in a percentage histogram. This is the Java equivalent of the + * UMA_HISTOGRAM_PERCENTAGE C++ macro. + * @param name name of the histogram + * @param sample sample to be recorded, at least 0 and at most 100. + */ + public static void recordPercentageHistogram(String name, int sample) { + if (sDisabledBy != null) return; + long key = getCachedHistogramKey(name); + long result = nativeRecordEnumeratedHistogram(name, key, sample, 101); + if (result != key) sCache.put(name, result); + } + + /** + * Records a sparse histogram. This is the Java equivalent of UmaHistogramSparse. + * @param name name of the histogram + * @param sample sample to be recorded. All values of |sample| are valid, including negative + * values. + */ + public static void recordSparseSlowlyHistogram(String name, int sample) { + if (sDisabledBy != null) return; + long key = getCachedHistogramKey(name); + long result = nativeRecordSparseHistogram(name, key, sample); + if (result != key) sCache.put(name, result); + } + + /** + * Records a sample in a histogram of times. Useful for recording short durations. This is the + * Java equivalent of the UMA_HISTOGRAM_TIMES C++ macro. + * Note that histogram samples will always be converted to milliseconds when logged. + * @param name name of the histogram + * @param duration duration to be recorded + * @param timeUnit the unit of the duration argument (must be >= MILLISECONDS) + */ + public static void recordTimesHistogram(String name, long duration, TimeUnit timeUnit) { + assertTimesHistogramSupportsUnit(timeUnit); + recordCustomTimesHistogramMilliseconds( + name, timeUnit.toMillis(duration), 1, TimeUnit.SECONDS.toMillis(10), 50); + } + + /** + * Records a sample in a histogram of times. Useful for recording medium durations. This is the + * Java equivalent of the UMA_HISTOGRAM_MEDIUM_TIMES C++ macro. + * Note that histogram samples will always be converted to milliseconds when logged. + * @param name name of the histogram + * @param duration duration to be recorded + * @param timeUnit the unit of the duration argument (must be >= MILLISECONDS) + */ + public static void recordMediumTimesHistogram(String name, long duration, TimeUnit timeUnit) { + assertTimesHistogramSupportsUnit(timeUnit); + recordCustomTimesHistogramMilliseconds( + name, timeUnit.toMillis(duration), 10, TimeUnit.MINUTES.toMillis(3), 50); + } + + /** + * Records a sample in a histogram of times. Useful for recording long durations. This is the + * Java equivalent of the UMA_HISTOGRAM_LONG_TIMES C++ macro. + * Note that histogram samples will always be converted to milliseconds when logged. + * @param name name of the histogram + * @param duration duration to be recorded + * @param timeUnit the unit of the duration argument (must be >= MILLISECONDS) + */ + public static void recordLongTimesHistogram(String name, long duration, TimeUnit timeUnit) { + assertTimesHistogramSupportsUnit(timeUnit); + recordCustomTimesHistogramMilliseconds( + name, timeUnit.toMillis(duration), 1, TimeUnit.HOURS.toMillis(1), 50); + } + + /** + * Records a sample in a histogram of times. Useful for recording long durations. This is the + * Java equivalent of the UMA_HISTOGRAM_LONG_TIMES_100 C++ macro. + * Note that histogram samples will always be converted to milliseconds when logged. + * @param name name of the histogram + * @param duration duration to be recorded + * @param timeUnit the unit of the duration argument (must be >= MILLISECONDS) + */ + public static void recordLongTimesHistogram100(String name, long duration, TimeUnit timeUnit) { + assertTimesHistogramSupportsUnit(timeUnit); + recordCustomTimesHistogramMilliseconds( + name, timeUnit.toMillis(duration), 1, TimeUnit.HOURS.toMillis(1), 100); + } + + /** + * Records a sample in a histogram of times with custom buckets. This is the Java equivalent of + * the UMA_HISTOGRAM_CUSTOM_TIMES C++ macro. + * Note that histogram samples will always be converted to milliseconds when logged. + * @param name name of the histogram + * @param duration duration to be recorded + * @param min the minimum bucket value + * @param max the maximum bucket value + * @param timeUnit the unit of the duration, min, and max arguments (must be >= MILLISECONDS) + * @param numBuckets the number of buckets + */ + public static void recordCustomTimesHistogram( + String name, long duration, long min, long max, TimeUnit timeUnit, int numBuckets) { + assertTimesHistogramSupportsUnit(timeUnit); + recordCustomTimesHistogramMilliseconds(name, timeUnit.toMillis(duration), + timeUnit.toMillis(min), timeUnit.toMillis(max), numBuckets); + } + + /** + * Records a sample in a histogram of sizes in KB. This is the Java equivalent of the + * UMA_HISTOGRAM_MEMORY_KB C++ macro. + * + * Good for sizes up to about 500MB. + * + * @param name name of the histogram. + * @param sizeInkB Sample to record in KB. + */ + public static void recordMemoryKBHistogram(String name, int sizeInKB) { + recordCustomCountHistogram(name, sizeInKB, 1000, 500000, 50); + } + + /** + * Asserts that the time unit is supported by TimesHistogram. + * @param timeUnit the unit, must be >= MILLISECONDS + */ + /* package */ static void assertTimesHistogramSupportsUnit(TimeUnit timeUnit) { + // Use extra variable, or else 'git cl format' produces weird results. + boolean supported = timeUnit != TimeUnit.NANOSECONDS && timeUnit != TimeUnit.MICROSECONDS; + assert supported : "TimesHistogram doesn't support MICROSECOND and NANOSECONDS time units. " + + "Consider using CountHistogram instead."; + } + + private static int clampToInt(long value) { + if (value > Integer.MAX_VALUE) return Integer.MAX_VALUE; + // Note: Clamping to MIN_VALUE rather than 0, to let base/ histograms code + // do its own handling of negative values in the future. + if (value < Integer.MIN_VALUE) return Integer.MIN_VALUE; + return (int) value; + } + + private static void recordCustomTimesHistogramMilliseconds( + String name, long duration, long min, long max, int numBuckets) { + if (sDisabledBy != null) return; + long key = getCachedHistogramKey(name); + // Note: Duration, min and max are clamped to int here because that's what's expected by + // the native histograms API. Callers of these functions still pass longs because that's + // the types returned by TimeUnit and System.currentTimeMillis() APIs, from which these + // values come. + assert max == clampToInt(max); + long result = nativeRecordCustomTimesHistogramMilliseconds( + name, key, clampToInt(duration), clampToInt(min), clampToInt(max), numBuckets); + if (result != key) sCache.put(name, result); + } + + /** + * Returns the number of samples recorded in the given bucket of the given histogram. + * @param name name of the histogram to look up + * @param sample the bucket containing this sample value will be looked up + */ + @VisibleForTesting + public static int getHistogramValueCountForTesting(String name, int sample) { + return nativeGetHistogramValueCountForTesting(name, sample); + } + + /** + * Returns the number of samples recorded for the given histogram. + * @param name name of the histogram to look up. + */ + @VisibleForTesting + public static int getHistogramTotalCountForTesting(String name) { + return nativeGetHistogramTotalCountForTesting(name); + } + + private static native long nativeRecordCustomTimesHistogramMilliseconds( + String name, long key, int duration, int min, int max, int numBuckets); + + private static native long nativeRecordBooleanHistogram(String name, long key, boolean sample); + private static native long nativeRecordEnumeratedHistogram( + String name, long key, int sample, int boundary); + private static native long nativeRecordCustomCountHistogram( + String name, long key, int sample, int min, int max, int numBuckets); + private static native long nativeRecordLinearCountHistogram( + String name, long key, int sample, int min, int max, int numBuckets); + private static native long nativeRecordSparseHistogram(String name, long key, int sample); + + private static native int nativeGetHistogramValueCountForTesting(String name, int sample); + private static native int nativeGetHistogramTotalCountForTesting(String name); +} diff --git a/base/android/java/src/org/chromium/base/metrics/RecordUserAction.java b/base/android/java/src/org/chromium/base/metrics/RecordUserAction.java new file mode 100644 index 0000000000..0d2ba548d2 --- /dev/null +++ b/base/android/java/src/org/chromium/base/metrics/RecordUserAction.java @@ -0,0 +1,85 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.metrics; + +import org.chromium.base.ThreadUtils; +import org.chromium.base.VisibleForTesting; +import org.chromium.base.annotations.CalledByNative; +import org.chromium.base.annotations.JNINamespace; + +/** + * Java API for recording UMA actions. + * + * WARNINGS: + * JNI calls are relatively costly - avoid using in performance-critical code. + * + * We use a script (extract_actions.py) to scan the source code and extract actions. A string + * literal (not a variable) must be passed to record(). + */ +@JNINamespace("base::android") +public class RecordUserAction { + private static Throwable sDisabledBy; + + /** + * Tests may not have native initialized, so they may need to disable metrics. The value should + * be reset after the test done, to avoid carrying over state to unrelated tests. + */ + @VisibleForTesting + public static void setDisabledForTests(boolean disabled) { + if (disabled && sDisabledBy != null) { + throw new IllegalStateException("UserActions are already disabled.", sDisabledBy); + } + sDisabledBy = disabled ? new Throwable() : null; + } + + public static void record(final String action) { + if (sDisabledBy != null) return; + + if (ThreadUtils.runningOnUiThread()) { + nativeRecordUserAction(action); + return; + } + + ThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + nativeRecordUserAction(action); + } + }); + } + + /** + * Interface to a class that receives a callback for each UserAction that is recorded. + */ + public interface UserActionCallback { + @CalledByNative("UserActionCallback") + void onActionRecorded(String action); + } + + private static long sNativeActionCallback; + + /** + * Register a callback that is executed for each recorded UserAction. + * Only one callback can be registered at a time. + * The callback has to be unregistered using removeActionCallbackForTesting(). + */ + public static void setActionCallbackForTesting(UserActionCallback callback) { + assert sNativeActionCallback == 0; + sNativeActionCallback = nativeAddActionCallbackForTesting(callback); + } + + /** + * Unregister the UserActionCallback. + */ + public static void removeActionCallbackForTesting() { + assert sNativeActionCallback != 0; + nativeRemoveActionCallbackForTesting(sNativeActionCallback); + sNativeActionCallback = 0; + } + + private static native void nativeRecordUserAction(String action); + private static native long nativeAddActionCallbackForTesting(UserActionCallback callback); + private static native void nativeRemoveActionCallbackForTesting(long callbackId); +} diff --git a/base/android/java/src/org/chromium/base/metrics/StatisticsRecorderAndroid.java b/base/android/java/src/org/chromium/base/metrics/StatisticsRecorderAndroid.java new file mode 100644 index 0000000000..bff3fae763 --- /dev/null +++ b/base/android/java/src/org/chromium/base/metrics/StatisticsRecorderAndroid.java @@ -0,0 +1,27 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.metrics; + +import org.chromium.base.annotations.JNINamespace; + +/** + * Java API which exposes the registered histograms on the native side as + * JSON test. + */ +@JNINamespace("base::android") +public final class StatisticsRecorderAndroid { + private StatisticsRecorderAndroid() {} + + /** + * @param verbosityLevel controls the information that should be included when dumping each of + * the histogram. + * @return All the registered histograms as JSON text. + */ + public static String toJson(@JSONVerbosityLevel int verbosityLevel) { + return nativeToJson(verbosityLevel); + } + + private static native String nativeToJson(@JSONVerbosityLevel int verbosityLevel); +}
\ No newline at end of file diff --git a/base/android/java/src/org/chromium/base/multidex/ChromiumMultiDexInstaller.java b/base/android/java/src/org/chromium/base/multidex/ChromiumMultiDexInstaller.java new file mode 100644 index 0000000000..5588ec5bdf --- /dev/null +++ b/base/android/java/src/org/chromium/base/multidex/ChromiumMultiDexInstaller.java @@ -0,0 +1,78 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.multidex; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.support.multidex.MultiDex; + +import org.chromium.base.ContextUtils; +import org.chromium.base.Log; +import org.chromium.base.VisibleForTesting; +import org.chromium.base.annotations.MainDex; + +/** + * Performs multidex installation for non-isolated processes. + */ +@MainDex +public class ChromiumMultiDexInstaller { + private static final String TAG = "base_multidex"; + + /** + * Suffix for the meta-data tag in the AndroidManifext.xml that determines whether loading + * secondary dexes should be skipped for a given process name. + */ + private static final String IGNORE_MULTIDEX_KEY = ".ignore_multidex"; + + /** + * Installs secondary dexes if possible/necessary. + * + * Isolated processes (e.g. renderer processes) can't load secondary dex files on + * K and below, so we don't even try in that case. + * + * In release builds of app apks (as opposed to test apks), this is a no-op because: + * - multidex isn't necessary in release builds because we run proguard there and + * thus aren't threatening to hit the dex limit; and + * - calling MultiDex.install, even in the absence of secondary dexes, causes a + * significant regression in start-up time (crbug.com/525695). + * + * @param context The application context. + */ + @VisibleForTesting + public static void install(Context context) { + // TODO(jbudorick): Back out this version check once support for K & below works. + // http://crbug.com/512357 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP + && !shouldInstallMultiDex(context)) { + Log.i(TAG, "Skipping multidex installation: not needed for process."); + } else { + MultiDex.install(context); + Log.i(TAG, "Completed multidex installation."); + } + } + + // Determines whether MultiDex should be installed for the current process. Isolated + // Processes should skip MultiDex as they can not actually access the files on disk. + // Privileged processes need ot have all of their dependencies in the MainDex for + // performance reasons. + private static boolean shouldInstallMultiDex(Context context) { + if (ContextUtils.isIsolatedProcess()) { + return false; + } + String currentProcessName = ContextUtils.getProcessName(); + PackageManager packageManager = context.getPackageManager(); + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(context.getPackageName(), + PackageManager.GET_META_DATA); + if (appInfo == null || appInfo.metaData == null) return true; + return !appInfo.metaData.getBoolean(currentProcessName + IGNORE_MULTIDEX_KEY, false); + } catch (PackageManager.NameNotFoundException e) { + return true; + } + } + +} diff --git a/base/android/java/src/org/chromium/base/process_launcher/ChildConnectionAllocator.java b/base/android/java/src/org/chromium/base/process_launcher/ChildConnectionAllocator.java new file mode 100644 index 0000000000..43ae2591d4 --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/ChildConnectionAllocator.java @@ -0,0 +1,305 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; + +import org.chromium.base.Log; +import org.chromium.base.VisibleForTesting; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Queue; + +/** + * This class is responsible for allocating and managing connections to child + * process services. These connections are in a pool (the services are defined + * in the AndroidManifest.xml). + */ +public class ChildConnectionAllocator { + private static final String TAG = "ChildConnAllocator"; + + /** Factory interface. Used by tests to specialize created connections. */ + @VisibleForTesting + public interface ConnectionFactory { + ChildProcessConnection createConnection(Context context, ComponentName serviceName, + boolean bindToCaller, boolean bindAsExternalService, Bundle serviceBundle); + } + + /** Default implementation of the ConnectionFactory that creates actual connections. */ + private static class ConnectionFactoryImpl implements ConnectionFactory { + @Override + public ChildProcessConnection createConnection(Context context, ComponentName serviceName, + boolean bindToCaller, boolean bindAsExternalService, Bundle serviceBundle) { + return new ChildProcessConnection( + context, serviceName, bindToCaller, bindAsExternalService, serviceBundle); + } + } + + // Delay between the call to freeConnection and the connection actually beeing freed. + private static final long FREE_CONNECTION_DELAY_MILLIS = 1; + + // The handler of the thread on which all interations should happen. + private final Handler mLauncherHandler; + + // Connections to services. Indices of the array correspond to the service numbers. + private final ChildProcessConnection[] mChildProcessConnections; + + // Runnable which will be called when allocator wants to allocate a new connection, but does + // not have any more free slots. May be null. + private final Runnable mFreeSlotCallback; + private final String mPackageName; + private final String mServiceClassName; + private final boolean mBindToCaller; + private final boolean mBindAsExternalService; + private final boolean mUseStrongBinding; + + // The list of free (not bound) service indices. + private final ArrayList<Integer> mFreeConnectionIndices; + + private final Queue<Runnable> mPendingAllocations = new ArrayDeque<>(); + + private ConnectionFactory mConnectionFactory = new ConnectionFactoryImpl(); + + /** + * Factory method that retrieves the service name and number of service from the + * AndroidManifest.xml. + */ + public static ChildConnectionAllocator create(Context context, Handler launcherHandler, + Runnable freeSlotCallback, String packageName, String serviceClassName, + String numChildServicesManifestKey, boolean bindToCaller, boolean bindAsExternalService, + boolean useStrongBinding) { + int numServices = -1; + PackageManager packageManager = context.getPackageManager(); + try { + ApplicationInfo appInfo = + packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + if (appInfo.metaData != null) { + numServices = appInfo.metaData.getInt(numChildServicesManifestKey, -1); + } + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException("Could not get application info."); + } + + if (numServices < 0) { + throw new RuntimeException("Illegal meta data value for number of child services"); + } + + // Check that the service exists. + try { + // PackageManager#getServiceInfo() throws an exception if the service does not exist. + packageManager.getServiceInfo( + new ComponentName(packageName, serviceClassName + "0"), 0); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException("Illegal meta data value: the child service doesn't exist"); + } + + return new ChildConnectionAllocator(launcherHandler, freeSlotCallback, packageName, + serviceClassName, bindToCaller, bindAsExternalService, useStrongBinding, + numServices); + } + + /** + * Factory method used with some tests to create an allocator with values passed in directly + * instead of being retrieved from the AndroidManifest.xml. + */ + @VisibleForTesting + public static ChildConnectionAllocator createForTest(Runnable freeSlotCallback, + String packageName, String serviceClassName, int serviceCount, boolean bindToCaller, + boolean bindAsExternalService, boolean useStrongBinding) { + return new ChildConnectionAllocator(new Handler(), freeSlotCallback, packageName, + serviceClassName, bindToCaller, bindAsExternalService, useStrongBinding, + serviceCount); + } + + private ChildConnectionAllocator(Handler launcherHandler, Runnable freeSlotCallback, + String packageName, String serviceClassName, boolean bindToCaller, + boolean bindAsExternalService, boolean useStrongBinding, int numChildServices) { + mFreeSlotCallback = freeSlotCallback; + mLauncherHandler = launcherHandler; + assert isRunningOnLauncherThread(); + mPackageName = packageName; + mServiceClassName = serviceClassName; + mBindToCaller = bindToCaller; + mBindAsExternalService = bindAsExternalService; + mUseStrongBinding = useStrongBinding; + mChildProcessConnections = new ChildProcessConnection[numChildServices]; + mFreeConnectionIndices = new ArrayList<Integer>(numChildServices); + for (int i = 0; i < numChildServices; i++) { + mFreeConnectionIndices.add(i); + } + } + + /** @return a bound connection, or null if there are no free slots. */ + public ChildProcessConnection allocate(Context context, Bundle serviceBundle, + final ChildProcessConnection.ServiceCallback serviceCallback) { + assert isRunningOnLauncherThread(); + if (mFreeConnectionIndices.isEmpty()) { + Log.d(TAG, "Ran out of services to allocate."); + return null; + } + int slot = mFreeConnectionIndices.remove(0); + assert mChildProcessConnections[slot] == null; + ComponentName serviceName = new ComponentName(mPackageName, mServiceClassName + slot); + + // Wrap the service callbacks so that: + // - we can intercept onChildProcessDied and clean-up connections + // - the callbacks are actually posted so that this method will return before the callbacks + // are called (so that the caller may set any reference to the returned connection before + // any callback logic potentially tries to access that connection). + ChildProcessConnection.ServiceCallback serviceCallbackWrapper = + new ChildProcessConnection.ServiceCallback() { + @Override + public void onChildStarted() { + assert isRunningOnLauncherThread(); + if (serviceCallback != null) { + mLauncherHandler.post(new Runnable() { + @Override + public void run() { + serviceCallback.onChildStarted(); + } + }); + } + } + + @Override + public void onChildStartFailed(final ChildProcessConnection connection) { + assert isRunningOnLauncherThread(); + if (serviceCallback != null) { + mLauncherHandler.post(new Runnable() { + @Override + public void run() { + serviceCallback.onChildStartFailed(connection); + } + }); + } + freeConnectionWithDelay(connection); + } + + @Override + public void onChildProcessDied(final ChildProcessConnection connection) { + assert isRunningOnLauncherThread(); + if (serviceCallback != null) { + mLauncherHandler.post(new Runnable() { + @Override + public void run() { + serviceCallback.onChildProcessDied(connection); + } + }); + } + freeConnectionWithDelay(connection); + } + + private void freeConnectionWithDelay(final ChildProcessConnection connection) { + // Freeing a service should be delayed. This is so that we avoid immediately + // reusing the freed service (see http://crbug.com/164069): the framework + // might keep a service process alive when it's been unbound for a short + // time. If a new connection to the same service is bound at that point, the + // process is reused and bad things happen (mostly static variables are set + // when we don't expect them to). + mLauncherHandler.postDelayed(new Runnable() { + @Override + public void run() { + free(connection); + } + }, FREE_CONNECTION_DELAY_MILLIS); + } + }; + + ChildProcessConnection connection = mConnectionFactory.createConnection( + context, serviceName, mBindToCaller, mBindAsExternalService, serviceBundle); + mChildProcessConnections[slot] = connection; + + connection.start(mUseStrongBinding, serviceCallbackWrapper); + Log.d(TAG, "Allocator allocated and bound a connection, name: %s, slot: %d", + mServiceClassName, slot); + return connection; + } + + /** Frees a connection and notifies listeners. */ + private void free(ChildProcessConnection connection) { + assert isRunningOnLauncherThread(); + + // mChildProcessConnections is relatively short (20 items at max at this point). + // We are better of iterating than caching in a map. + int slot = Arrays.asList(mChildProcessConnections).indexOf(connection); + if (slot == -1) { + Log.e(TAG, "Unable to find connection to free."); + assert false; + } else { + mChildProcessConnections[slot] = null; + assert !mFreeConnectionIndices.contains(slot); + mFreeConnectionIndices.add(slot); + Log.d(TAG, "Allocator freed a connection, name: %s, slot: %d", mServiceClassName, slot); + } + + if (mPendingAllocations.isEmpty()) return; + mPendingAllocations.remove().run(); + assert mFreeConnectionIndices.isEmpty(); + if (!mPendingAllocations.isEmpty() && mFreeSlotCallback != null) mFreeSlotCallback.run(); + } + + // Can only be called once all slots are full, ie when allocate returns null. + // The callback will be called when a slot becomes free, and should synchronous call + // allocate to take the slot. + public void queueAllocation(Runnable runnable) { + assert mFreeConnectionIndices.isEmpty(); + boolean wasEmpty = mPendingAllocations.isEmpty(); + mPendingAllocations.add(runnable); + if (wasEmpty && mFreeSlotCallback != null) mFreeSlotCallback.run(); + } + + public String getPackageName() { + return mPackageName; + } + + public boolean anyConnectionAllocated() { + return mFreeConnectionIndices.size() < mChildProcessConnections.length; + } + + public boolean isFreeConnectionAvailable() { + assert isRunningOnLauncherThread(); + return !mFreeConnectionIndices.isEmpty(); + } + + public int getNumberOfServices() { + return mChildProcessConnections.length; + } + + public boolean isConnectionFromAllocator(ChildProcessConnection connection) { + for (ChildProcessConnection existingConnection : mChildProcessConnections) { + if (existingConnection == connection) return true; + } + return false; + } + + @VisibleForTesting + public void setConnectionFactoryForTesting(ConnectionFactory connectionFactory) { + mConnectionFactory = connectionFactory; + } + + /** @return the count of connections managed by the allocator */ + @VisibleForTesting + public int allocatedConnectionsCountForTesting() { + assert isRunningOnLauncherThread(); + return mChildProcessConnections.length - mFreeConnectionIndices.size(); + } + + @VisibleForTesting + public ChildProcessConnection getChildProcessConnectionAtSlotForTesting(int slotNumber) { + return mChildProcessConnections[slotNumber]; + } + + private boolean isRunningOnLauncherThread() { + return mLauncherHandler.getLooper() == Looper.myLooper(); + } +} diff --git a/base/android/java/src/org/chromium/base/process_launcher/ChildProcessConnection.java b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessConnection.java new file mode 100644 index 0000000000..bfa5d5cc6a --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessConnection.java @@ -0,0 +1,766 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; + +import org.chromium.base.ChildBindingState; +import org.chromium.base.Log; +import org.chromium.base.MemoryPressureLevel; +import org.chromium.base.MemoryPressureListener; +import org.chromium.base.ThreadUtils; +import org.chromium.base.TraceEvent; +import org.chromium.base.VisibleForTesting; +import org.chromium.base.memory.MemoryPressureCallback; + +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; + +/** + * Manages a connection between the browser activity and a child service. + */ +public class ChildProcessConnection { + private static final String TAG = "ChildProcessConn"; + private static final int NUM_BINDING_STATES = ChildBindingState.MAX_VALUE + 1; + + /** + * Used to notify the consumer about the process start. These callbacks will be invoked before + * the ConnectionCallbacks. + */ + public interface ServiceCallback { + /** + * Called when the child process has successfully started and is ready for connection + * setup. + */ + void onChildStarted(); + + /** + * Called when the child process failed to start. This can happen if the process is already + * in use by another client. The client will not receive any other callbacks after this one. + */ + void onChildStartFailed(ChildProcessConnection connection); + + /** + * Called when the service has been disconnected. whether it was stopped by the client or + * if it stopped unexpectedly (process crash). + * This is the last callback from this interface that a client will receive for a specific + * connection. + */ + void onChildProcessDied(ChildProcessConnection connection); + } + + /** + * Used to notify the consumer about the connection being established. + */ + public interface ConnectionCallback { + /** + * Called when the connection to the service is established. + * @param connection the connection object to the child process + */ + void onConnected(ChildProcessConnection connection); + } + + /** + * Delegate that ChildServiceConnection should call when the service connects/disconnects. + * These callbacks are expected to happen on a background thread. + */ + @VisibleForTesting + protected interface ChildServiceConnectionDelegate { + void onServiceConnected(IBinder service); + void onServiceDisconnected(); + } + + @VisibleForTesting + protected interface ChildServiceConnectionFactory { + ChildServiceConnection createConnection( + Intent bindIntent, int bindFlags, ChildServiceConnectionDelegate delegate); + } + + /** Interface representing a connection to the Android service. Can be mocked in unit-tests. */ + @VisibleForTesting + protected interface ChildServiceConnection { + boolean bind(); + void unbind(); + boolean isBound(); + } + + /** Implementation of ChildServiceConnection that does connect to a service. */ + private static class ChildServiceConnectionImpl + implements ChildServiceConnection, ServiceConnection { + private final Context mContext; + private final Intent mBindIntent; + private final int mBindFlags; + private final ChildServiceConnectionDelegate mDelegate; + private boolean mBound; + + private ChildServiceConnectionImpl(Context context, Intent bindIntent, int bindFlags, + ChildServiceConnectionDelegate delegate) { + mContext = context; + mBindIntent = bindIntent; + mBindFlags = bindFlags; + mDelegate = delegate; + } + + @Override + public boolean bind() { + if (!mBound) { + try { + TraceEvent.begin("ChildProcessConnection.ChildServiceConnectionImpl.bind"); + mBound = mContext.bindService(mBindIntent, this, mBindFlags); + } finally { + TraceEvent.end("ChildProcessConnection.ChildServiceConnectionImpl.bind"); + } + } + return mBound; + } + + @Override + public void unbind() { + if (mBound) { + mContext.unbindService(this); + mBound = false; + } + } + + @Override + public boolean isBound() { + return mBound; + } + + @Override + public void onServiceConnected(ComponentName className, final IBinder service) { + mDelegate.onServiceConnected(service); + } + + // Called on the main thread to notify that the child service did not disconnect gracefully. + @Override + public void onServiceDisconnected(ComponentName className) { + mDelegate.onServiceDisconnected(); + } + } + + // Synchronize on this for access. + @GuardedBy("sAllBindingStateCounts") + private static final int[] sAllBindingStateCounts = new int[NUM_BINDING_STATES]; + + @VisibleForTesting + static void resetBindingStateCountsForTesting() { + synchronized (sAllBindingStateCounts) { + for (int i = 0; i < NUM_BINDING_STATES; ++i) { + sAllBindingStateCounts[i] = 0; + } + } + } + + private final Handler mLauncherHandler; + private final ComponentName mServiceName; + + // Parameters passed to the child process through the service binding intent. + // If the service gets recreated by the framework the intent will be reused, so these parameters + // should be common to all processes of that type. + private final Bundle mServiceBundle; + + // Whether bindToCaller should be called on the service after setup to check that only one + // process is bound to the service. + private final boolean mBindToCaller; + + private static class ConnectionParams { + final Bundle mConnectionBundle; + final List<IBinder> mClientInterfaces; + + ConnectionParams(Bundle connectionBundle, List<IBinder> clientInterfaces) { + mConnectionBundle = connectionBundle; + mClientInterfaces = clientInterfaces; + } + } + + // This is set in start() and is used in onServiceConnected(). + private ServiceCallback mServiceCallback; + + // This is set in setupConnection() and is later used in doConnectionSetup(), after which the + // variable is cleared. Therefore this is only valid while the connection is being set up. + private ConnectionParams mConnectionParams; + + // Callback provided in setupConnection() that will communicate the result to the caller. This + // has to be called exactly once after setupConnection(), even if setup fails, so that the + // caller can free up resources associated with the setup attempt. This is set to null after the + // call. + private ConnectionCallback mConnectionCallback; + + private IChildProcessService mService; + + // Set to true when the service connection callback runs. This differs from + // mServiceConnectComplete, which tracks that the connection completed successfully. + private boolean mDidOnServiceConnected; + + // Set to true when the service connected successfully. + private boolean mServiceConnectComplete; + + // Set to true when the service disconnects, as opposed to being properly closed. This happens + // when the process crashes or gets killed by the system out-of-memory killer. + private boolean mServiceDisconnected; + + // Process ID of the corresponding child process. + private int mPid; + + // Strong binding will make the service priority equal to the priority of the activity. + private final ChildServiceConnection mStrongBinding; + + // Moderate binding will make the service priority equal to the priority of a visible process + // while the app is in the foreground. + // This is also used as the initial binding before any priorities are set. + private final ChildServiceConnection mModerateBinding; + + // Low priority binding maintained in the entire lifetime of the connection, i.e. between calls + // to start() and stop(). + private final ChildServiceConnection mWaivedBinding; + + // Refcount of bindings. + private int mStrongBindingCount; + private int mModerateBindingCount; + + // Set to true once unbind() was called. + private boolean mUnbound; + + // Binding state of this connection. + private @ChildBindingState int mBindingState; + + // Protects access to instance variables that are also accessed on the client thread. + private final Object mClientThreadLock = new Object(); + + // Same as above except it no longer updates after |unbind()|. + @GuardedBy("mClientThreadLock") + private @ChildBindingState int mBindingStateCurrentOrWhenDied; + + // Indicate |kill()| was called to intentionally kill this process. + @GuardedBy("mClientThreadLock") + private boolean mKilledByUs; + + // Copy of |sAllBindingStateCounts| at the time this is unbound. + @GuardedBy("mClientThreadLock") + private int[] mAllBindingStateCountsWhenDied; + + private MemoryPressureCallback mMemoryPressureCallback; + + public ChildProcessConnection(Context context, ComponentName serviceName, boolean bindToCaller, + boolean bindAsExternalService, Bundle serviceBundle) { + this(context, serviceName, bindToCaller, bindAsExternalService, serviceBundle, + null /* connectionFactory */); + } + + @VisibleForTesting + public ChildProcessConnection(final Context context, ComponentName serviceName, + boolean bindToCaller, boolean bindAsExternalService, Bundle serviceBundle, + ChildServiceConnectionFactory connectionFactory) { + mLauncherHandler = new Handler(); + assert isRunningOnLauncherThread(); + mServiceName = serviceName; + mServiceBundle = serviceBundle != null ? serviceBundle : new Bundle(); + mServiceBundle.putBoolean(ChildProcessConstants.EXTRA_BIND_TO_CALLER, bindToCaller); + mBindToCaller = bindToCaller; + + if (connectionFactory == null) { + connectionFactory = new ChildServiceConnectionFactory() { + @Override + public ChildServiceConnection createConnection( + Intent bindIntent, int bindFlags, ChildServiceConnectionDelegate delegate) { + return new ChildServiceConnectionImpl(context, bindIntent, bindFlags, delegate); + } + }; + } + + ChildServiceConnectionDelegate delegate = new ChildServiceConnectionDelegate() { + @Override + public void onServiceConnected(final IBinder service) { + mLauncherHandler.post(new Runnable() { + @Override + public void run() { + onServiceConnectedOnLauncherThread(service); + } + }); + } + + @Override + public void onServiceDisconnected() { + mLauncherHandler.post(new Runnable() { + @Override + public void run() { + onServiceDisconnectedOnLauncherThread(); + } + }); + } + }; + + Intent intent = new Intent(); + intent.setComponent(serviceName); + if (serviceBundle != null) { + intent.putExtras(serviceBundle); + } + + int defaultFlags = Context.BIND_AUTO_CREATE + | (bindAsExternalService ? Context.BIND_EXTERNAL_SERVICE : 0); + + mModerateBinding = connectionFactory.createConnection(intent, defaultFlags, delegate); + mStrongBinding = connectionFactory.createConnection( + intent, defaultFlags | Context.BIND_IMPORTANT, delegate); + mWaivedBinding = connectionFactory.createConnection( + intent, defaultFlags | Context.BIND_WAIVE_PRIORITY, delegate); + } + + public final IChildProcessService getService() { + assert isRunningOnLauncherThread(); + return mService; + } + + public final ComponentName getServiceName() { + assert isRunningOnLauncherThread(); + return mServiceName; + } + + public boolean isConnected() { + return mService != null; + } + + /** + * @return the connection pid, or 0 if not yet connected + */ + public int getPid() { + assert isRunningOnLauncherThread(); + return mPid; + } + + /** + * Starts a connection to an IChildProcessService. This must be followed by a call to + * setupConnection() to setup the connection parameters. start() and setupConnection() are + * separate to allow to pass whatever parameters are available in start(), and complete the + * remainder addStrongBinding while reducing the connection setup latency. + * @param useStrongBinding whether a strong binding should be bound by default. If false, an + * initial moderate binding is used. + * @param serviceCallback (optional) callbacks invoked when the child process starts or fails to + * start and when the service stops. + */ + public void start(boolean useStrongBinding, ServiceCallback serviceCallback) { + try { + TraceEvent.begin("ChildProcessConnection.start"); + assert isRunningOnLauncherThread(); + assert mConnectionParams + == null : "setupConnection() called before start() in ChildProcessConnection."; + + mServiceCallback = serviceCallback; + + if (!bind(useStrongBinding)) { + Log.e(TAG, "Failed to establish the service connection."); + // We have to notify the caller so that they can free-up associated resources. + // TODO(ppi): Can we hard-fail here? + notifyChildProcessDied(); + } + } finally { + TraceEvent.end("ChildProcessConnection.start"); + } + } + + /** + * Sets-up the connection after it was started with start(). + * @param connectionBundle a bundle passed to the service that can be used to pass various + * parameters to the service + * @param clientInterfaces optional client specified interfaces that the child can use to + * communicate with the parent process + * @param connectionCallback will be called exactly once after the connection is set up or the + * setup fails + */ + public void setupConnection(Bundle connectionBundle, @Nullable List<IBinder> clientInterfaces, + ConnectionCallback connectionCallback) { + assert isRunningOnLauncherThread(); + assert mConnectionParams == null; + if (mServiceDisconnected) { + Log.w(TAG, "Tried to setup a connection that already disconnected."); + connectionCallback.onConnected(null); + return; + } + try { + TraceEvent.begin("ChildProcessConnection.setupConnection"); + mConnectionCallback = connectionCallback; + mConnectionParams = new ConnectionParams(connectionBundle, clientInterfaces); + // Run the setup if the service is already connected. If not, doConnectionSetup() will + // be called from onServiceConnected(). + if (mServiceConnectComplete) { + doConnectionSetup(); + } + } finally { + TraceEvent.end("ChildProcessConnection.setupConnection"); + } + } + + /** + * Terminates the connection to IChildProcessService, closing all bindings. It is safe to call + * this multiple times. + */ + public void stop() { + assert isRunningOnLauncherThread(); + unbind(); + notifyChildProcessDied(); + } + + public void kill() { + assert isRunningOnLauncherThread(); + IChildProcessService service = mService; + unbind(); + try { + if (service != null) service.forceKill(); + } catch (RemoteException e) { + // Intentionally ignore since we are killing it anyway. + } + synchronized (mClientThreadLock) { + mKilledByUs = true; + } + notifyChildProcessDied(); + } + + private void onServiceConnectedOnLauncherThread(IBinder service) { + assert isRunningOnLauncherThread(); + // A flag from the parent class ensures we run the post-connection logic only once + // (instead of once per each ChildServiceConnection). + if (mDidOnServiceConnected) { + return; + } + try { + TraceEvent.begin("ChildProcessConnection.ChildServiceConnection.onServiceConnected"); + mDidOnServiceConnected = true; + mService = IChildProcessService.Stub.asInterface(service); + + if (mBindToCaller) { + try { + if (!mService.bindToCaller()) { + if (mServiceCallback != null) { + mServiceCallback.onChildStartFailed(this); + } + unbind(); + return; + } + } catch (RemoteException ex) { + // Do not trigger the StartCallback here, since the service is already + // dead and the onChildStopped callback will run from onServiceDisconnected(). + Log.e(TAG, "Failed to bind service to connection.", ex); + return; + } + } + + if (mServiceCallback != null) { + mServiceCallback.onChildStarted(); + } + + mServiceConnectComplete = true; + + if (mMemoryPressureCallback == null) { + final MemoryPressureCallback callback = this ::onMemoryPressure; + ThreadUtils.postOnUiThread(() -> MemoryPressureListener.addCallback(callback)); + mMemoryPressureCallback = callback; + } + + // Run the setup if the connection parameters have already been provided. If + // not, doConnectionSetup() will be called from setupConnection(). + if (mConnectionParams != null) { + doConnectionSetup(); + } + } finally { + TraceEvent.end("ChildProcessConnection.ChildServiceConnection.onServiceConnected"); + } + } + + private void onServiceDisconnectedOnLauncherThread() { + assert isRunningOnLauncherThread(); + // Ensure that the disconnection logic runs only once (instead of once per each + // ChildServiceConnection). + if (mServiceDisconnected) { + return; + } + mServiceDisconnected = true; + Log.w(TAG, "onServiceDisconnected (crash or killed by oom): pid=%d", mPid); + stop(); // We don't want to auto-restart on crash. Let the browser do that. + + // If we have a pending connection callback, we need to communicate the failure to + // the caller. + if (mConnectionCallback != null) { + mConnectionCallback.onConnected(null); + mConnectionCallback = null; + } + } + + private void onSetupConnectionResult(int pid) { + mPid = pid; + assert mPid != 0 : "Child service claims to be run by a process of pid=0."; + + if (mConnectionCallback != null) { + mConnectionCallback.onConnected(this); + } + mConnectionCallback = null; + } + + /** + * Called after the connection parameters have been set (in setupConnection()) *and* a + * connection has been established (as signaled by onServiceConnected()). These two events can + * happen in any order. + */ + private void doConnectionSetup() { + try { + TraceEvent.begin("ChildProcessConnection.doConnectionSetup"); + assert mServiceConnectComplete && mService != null; + assert mConnectionParams != null; + + ICallbackInt pidCallback = new ICallbackInt.Stub() { + @Override + public void call(final int pid) { + mLauncherHandler.post(new Runnable() { + @Override + public void run() { + onSetupConnectionResult(pid); + } + }); + } + }; + try { + mService.setupConnection(mConnectionParams.mConnectionBundle, pidCallback, + mConnectionParams.mClientInterfaces); + } catch (RemoteException re) { + Log.e(TAG, "Failed to setup connection.", re); + } + mConnectionParams = null; + } finally { + TraceEvent.end("ChildProcessConnection.doConnectionSetup"); + } + } + + private boolean bind(boolean useStrongBinding) { + assert isRunningOnLauncherThread(); + assert !mUnbound; + + boolean success; + if (useStrongBinding) { + success = mStrongBinding.bind(); + } else { + mModerateBindingCount++; + success = mModerateBinding.bind(); + } + if (!success) return false; + + mWaivedBinding.bind(); + updateBindingState(); + return true; + } + + @VisibleForTesting + protected void unbind() { + assert isRunningOnLauncherThread(); + mService = null; + mConnectionParams = null; + mUnbound = true; + mStrongBinding.unbind(); + mWaivedBinding.unbind(); + mModerateBinding.unbind(); + updateBindingState(); + + int[] bindingStateCounts; + synchronized (sAllBindingStateCounts) { + bindingStateCounts = Arrays.copyOf(sAllBindingStateCounts, NUM_BINDING_STATES); + } + synchronized (mClientThreadLock) { + mAllBindingStateCountsWhenDied = bindingStateCounts; + } + + if (mMemoryPressureCallback != null) { + final MemoryPressureCallback callback = mMemoryPressureCallback; + ThreadUtils.postOnUiThread(() -> MemoryPressureListener.removeCallback(callback)); + mMemoryPressureCallback = null; + } + } + + public boolean isStrongBindingBound() { + assert isRunningOnLauncherThread(); + return mStrongBinding.isBound(); + } + + public void addStrongBinding() { + assert isRunningOnLauncherThread(); + if (!isConnected()) { + Log.w(TAG, "The connection is not bound for %d", getPid()); + return; + } + if (mStrongBindingCount == 0) { + mStrongBinding.bind(); + updateBindingState(); + } + mStrongBindingCount++; + } + + public void removeStrongBinding() { + assert isRunningOnLauncherThread(); + if (!isConnected()) { + Log.w(TAG, "The connection is not bound for %d", getPid()); + return; + } + assert mStrongBindingCount > 0; + mStrongBindingCount--; + if (mStrongBindingCount == 0) { + mStrongBinding.unbind(); + updateBindingState(); + } + } + + public boolean isModerateBindingBound() { + assert isRunningOnLauncherThread(); + return mModerateBinding.isBound(); + } + + public void addModerateBinding() { + assert isRunningOnLauncherThread(); + if (!isConnected()) { + Log.w(TAG, "The connection is not bound for %d", getPid()); + return; + } + if (mModerateBindingCount == 0) { + mModerateBinding.bind(); + updateBindingState(); + } + mModerateBindingCount++; + } + + public void removeModerateBinding() { + assert isRunningOnLauncherThread(); + if (!isConnected()) { + Log.w(TAG, "The connection is not bound for %d", getPid()); + return; + } + assert mModerateBindingCount > 0; + mModerateBindingCount--; + if (mModerateBindingCount == 0) { + mModerateBinding.unbind(); + updateBindingState(); + } + } + + /** + * @return true if the connection is bound and only bound with the waived binding or if the + * connection is unbound and was only bound with the waived binding when it disconnected. + */ + public @ChildBindingState int bindingStateCurrentOrWhenDied() { + // WARNING: this method can be called from a thread other than the launcher thread. + // Note that it returns the current waived bound only state and is racy. This not really + // preventable without changing the caller's API, short of blocking. + synchronized (mClientThreadLock) { + return mBindingStateCurrentOrWhenDied; + } + } + + /** + * @return true if the connection is intentionally killed by calling kill(). + */ + public boolean isKilledByUs() { + // WARNING: this method can be called from a thread other than the launcher thread. + // Note that it returns the current waived bound only state and is racy. This not really + // preventable without changing the caller's API, short of blocking. + synchronized (mClientThreadLock) { + return mKilledByUs; + } + } + + public int[] bindingStateCountsCurrentOrWhenDied() { + // WARNING: this method can be called from a thread other than the launcher thread. + // Note that it returns the current waived bound only state and is racy. This not really + // preventable without changing the caller's API, short of blocking. + synchronized (mClientThreadLock) { + if (mAllBindingStateCountsWhenDied != null) { + return Arrays.copyOf(mAllBindingStateCountsWhenDied, NUM_BINDING_STATES); + } + } + synchronized (sAllBindingStateCounts) { + return Arrays.copyOf(sAllBindingStateCounts, NUM_BINDING_STATES); + } + } + + // Should be called any binding is bound or unbound. + private void updateBindingState() { + int oldBindingState = mBindingState; + if (mUnbound) { + mBindingState = ChildBindingState.UNBOUND; + } else if (mStrongBinding.isBound()) { + mBindingState = ChildBindingState.STRONG; + } else if (mModerateBinding.isBound()) { + mBindingState = ChildBindingState.MODERATE; + } else { + assert mWaivedBinding.isBound(); + mBindingState = ChildBindingState.WAIVED; + } + + if (mBindingState != oldBindingState) { + synchronized (sAllBindingStateCounts) { + if (oldBindingState != ChildBindingState.UNBOUND) { + assert sAllBindingStateCounts[oldBindingState] > 0; + sAllBindingStateCounts[oldBindingState]--; + } + if (mBindingState != ChildBindingState.UNBOUND) { + sAllBindingStateCounts[mBindingState]++; + } + } + } + + if (!mUnbound) { + synchronized (mClientThreadLock) { + mBindingStateCurrentOrWhenDied = mBindingState; + } + } + } + + private void notifyChildProcessDied() { + if (mServiceCallback != null) { + // Guard against nested calls to this method. + ServiceCallback serviceCallback = mServiceCallback; + mServiceCallback = null; + serviceCallback.onChildProcessDied(this); + } + } + + private boolean isRunningOnLauncherThread() { + return mLauncherHandler.getLooper() == Looper.myLooper(); + } + + @VisibleForTesting + public void crashServiceForTesting() throws RemoteException { + mService.forceKill(); + } + + @VisibleForTesting + public boolean didOnServiceConnectedForTesting() { + return mDidOnServiceConnected; + } + + @VisibleForTesting + protected Handler getLauncherHandler() { + return mLauncherHandler; + } + + private void onMemoryPressure(@MemoryPressureLevel int pressure) { + mLauncherHandler.post(() -> onMemoryPressureOnLauncherThread(pressure)); + } + + private void onMemoryPressureOnLauncherThread(@MemoryPressureLevel int pressure) { + if (mService == null) return; + try { + mService.onMemoryPressure(pressure); + } catch (RemoteException ex) { + // Ignore + } + } +} diff --git a/base/android/java/src/org/chromium/base/process_launcher/ChildProcessConstants.java b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessConstants.java new file mode 100644 index 0000000000..ec232d7c16 --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessConstants.java @@ -0,0 +1,27 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +/** + * Constants to be used by child processes. + */ +public interface ChildProcessConstants { + // Below are the names for the items placed in the bind or start command intent. + // Note that because that intent maybe reused if a service is restarted, none should be process + // specific. + + public static final String EXTRA_BIND_TO_CALLER = + "org.chromium.base.process_launcher.extra.bind_to_caller"; + + // Below are the names for the items placed in the Bundle passed in the + // IChildProcessService.setupConnection call, once the connection has been established. + + // Key for the command line. + public static final String EXTRA_COMMAND_LINE = + "org.chromium.base.process_launcher.extra.command_line"; + + // Key for the file descriptors that should be mapped in the child process. + public static final String EXTRA_FILES = "org.chromium.base.process_launcher.extra.extraFiles"; +} diff --git a/base/android/java/src/org/chromium/base/process_launcher/ChildProcessLauncher.java b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessLauncher.java new file mode 100644 index 0000000000..7cdc8528bd --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessLauncher.java @@ -0,0 +1,278 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; + +import org.chromium.base.ContextUtils; +import org.chromium.base.Log; +import org.chromium.base.TraceEvent; + +import java.io.IOException; +import java.util.List; + +/** + * This class is used to start a child process by connecting to a ChildProcessService. + */ +public class ChildProcessLauncher { + private static final String TAG = "ChildProcLauncher"; + + /** Delegate that client should use to customize the process launching. */ + public abstract static class Delegate { + /** + * Called when the launcher is about to start. Gives the embedder a chance to provide an + * already bound connection if it has one. (allowing for warm-up connections: connections + * that are already bound in advance to speed up child process start-up time). + * Note that onBeforeConnectionAllocated will not be called if this method returns a + * connection. + * @param connectionAllocator the allocator the returned connection should have been + * allocated of. + * @param serviceCallback the service callback that the connection should use. + * @return a bound connection to use to connect to the child process service, or null if a + * connection should be allocated and bound by the launcher. + */ + public ChildProcessConnection getBoundConnection( + ChildConnectionAllocator connectionAllocator, + ChildProcessConnection.ServiceCallback serviceCallback) { + return null; + } + + /** + * Called before a connection is allocated. + * Note that this is only called if the ChildProcessLauncher is created with + * {@link #createWithConnectionAllocator}. + * @param serviceBundle the bundle passed in the service intent. Clients can add their own + * extras to the bundle. + */ + public void onBeforeConnectionAllocated(Bundle serviceBundle) {} + + /** + * Called before setup is called on the connection. + * @param connectionBundle the bundle passed to the {@link ChildProcessService} in the + * setup call. Clients can add their own extras to the bundle. + */ + public void onBeforeConnectionSetup(Bundle connectionBundle) {} + + /** + * Called when the connection was successfully established, meaning the setup call on the + * service was successful. + * @param connection the connection over which the setup call was made. + */ + public void onConnectionEstablished(ChildProcessConnection connection) {} + + /** + * Called when a connection has been disconnected. Only invoked if onConnectionEstablished + * was called, meaning the connection was already established. + * @param connection the connection that got disconnected. + */ + public void onConnectionLost(ChildProcessConnection connection) {} + } + + // Represents an invalid process handle; same as base/process/process.h kNullProcessHandle. + private static final int NULL_PROCESS_HANDLE = 0; + + // The handle for the thread we were created on and on which all methods should be called. + private final Handler mLauncherHandler; + + private final Delegate mDelegate; + + private final String[] mCommandLine; + private final FileDescriptorInfo[] mFilesToBeMapped; + + // The allocator used to create the connection. + private final ChildConnectionAllocator mConnectionAllocator; + + // The IBinder interfaces provided to the created service. + private final List<IBinder> mClientInterfaces; + + // The actual service connection. Set once we have connected to the service. + private ChildProcessConnection mConnection; + + /** + * Constructor. + * + * @param launcherHandler the handler for the thread where all operations should happen. + * @param delegate the delagate that gets notified of the launch progress. + * @param commandLine the command line that should be passed to the started process. + * @param filesToBeMapped the files that should be passed to the started process. + * @param connectionAllocator the allocator used to create connections to the service. + * @param clientInterfaces the interfaces that should be passed to the started process so it can + * communicate with the parent process. + */ + public ChildProcessLauncher(Handler launcherHandler, Delegate delegate, String[] commandLine, + FileDescriptorInfo[] filesToBeMapped, ChildConnectionAllocator connectionAllocator, + List<IBinder> clientInterfaces) { + assert connectionAllocator != null; + mLauncherHandler = launcherHandler; + isRunningOnLauncherThread(); + mCommandLine = commandLine; + mConnectionAllocator = connectionAllocator; + mDelegate = delegate; + mFilesToBeMapped = filesToBeMapped; + mClientInterfaces = clientInterfaces; + } + + /** + * Starts the child process and calls setup on it if {@param setupConnection} is true. + * @param setupConnection whether the setup should be performed on the connection once + * established + * @param queueIfNoFreeConnection whether to queue that request if no service connection is + * available. If the launcher was created with a connection provider, this parameter has no + * effect. + * @return true if the connection was started or was queued. + */ + public boolean start(final boolean setupConnection, final boolean queueIfNoFreeConnection) { + assert isRunningOnLauncherThread(); + try { + TraceEvent.begin("ChildProcessLauncher.start"); + ChildProcessConnection.ServiceCallback serviceCallback = + new ChildProcessConnection.ServiceCallback() { + @Override + public void onChildStarted() {} + + @Override + public void onChildStartFailed(ChildProcessConnection connection) { + assert isRunningOnLauncherThread(); + assert mConnection == connection; + Log.e(TAG, "ChildProcessConnection.start failed, trying again"); + mLauncherHandler.post(new Runnable() { + @Override + public void run() { + // The child process may already be bound to another client + // (this can happen if multi-process WebView is used in more + // than one process), so try starting the process again. + // This connection that failed to start has not been freed, + // so a new bound connection will be allocated. + mConnection = null; + start(setupConnection, queueIfNoFreeConnection); + } + }); + } + + @Override + public void onChildProcessDied(ChildProcessConnection connection) { + assert isRunningOnLauncherThread(); + assert mConnection == connection; + ChildProcessLauncher.this.onChildProcessDied(); + } + }; + mConnection = mDelegate.getBoundConnection(mConnectionAllocator, serviceCallback); + if (mConnection != null) { + assert mConnectionAllocator.isConnectionFromAllocator(mConnection); + setupConnection(); + return true; + } + if (!allocateAndSetupConnection( + serviceCallback, setupConnection, queueIfNoFreeConnection) + && !queueIfNoFreeConnection) { + return false; + } + return true; + } finally { + TraceEvent.end("ChildProcessLauncher.start"); + } + } + + public ChildProcessConnection getConnection() { + return mConnection; + } + + public ChildConnectionAllocator getConnectionAllocator() { + return mConnectionAllocator; + } + + private boolean allocateAndSetupConnection( + final ChildProcessConnection.ServiceCallback serviceCallback, + final boolean setupConnection, final boolean queueIfNoFreeConnection) { + assert mConnection == null; + Bundle serviceBundle = new Bundle(); + mDelegate.onBeforeConnectionAllocated(serviceBundle); + + mConnection = mConnectionAllocator.allocate( + ContextUtils.getApplicationContext(), serviceBundle, serviceCallback); + if (mConnection == null) { + if (!queueIfNoFreeConnection) { + Log.d(TAG, "Failed to allocate a child connection (no queuing)."); + return false; + } + mConnectionAllocator.queueAllocation( + () -> allocateAndSetupConnection( + serviceCallback, setupConnection, queueIfNoFreeConnection)); + return false; + } + + if (setupConnection) { + setupConnection(); + } + return true; + } + + private void setupConnection() { + ChildProcessConnection.ConnectionCallback connectionCallback = + new ChildProcessConnection.ConnectionCallback() { + @Override + public void onConnected(ChildProcessConnection connection) { + assert mConnection == connection; + onServiceConnected(); + } + }; + Bundle connectionBundle = createConnectionBundle(); + mDelegate.onBeforeConnectionSetup(connectionBundle); + mConnection.setupConnection(connectionBundle, getClientInterfaces(), connectionCallback); + } + + private void onServiceConnected() { + assert isRunningOnLauncherThread(); + + Log.d(TAG, "on connect callback, pid=%d", mConnection.getPid()); + + mDelegate.onConnectionEstablished(mConnection); + + // Proactively close the FDs rather than waiting for the GC to do it. + try { + for (FileDescriptorInfo fileInfo : mFilesToBeMapped) { + fileInfo.fd.close(); + } + } catch (IOException ioe) { + Log.w(TAG, "Failed to close FD.", ioe); + } + } + + public int getPid() { + assert isRunningOnLauncherThread(); + return mConnection == null ? NULL_PROCESS_HANDLE : mConnection.getPid(); + } + + public List<IBinder> getClientInterfaces() { + return mClientInterfaces; + } + + private boolean isRunningOnLauncherThread() { + return mLauncherHandler.getLooper() == Looper.myLooper(); + } + + private Bundle createConnectionBundle() { + Bundle bundle = new Bundle(); + bundle.putStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE, mCommandLine); + bundle.putParcelableArray(ChildProcessConstants.EXTRA_FILES, mFilesToBeMapped); + return bundle; + } + + private void onChildProcessDied() { + assert isRunningOnLauncherThread(); + if (getPid() != 0) { + mDelegate.onConnectionLost(mConnection); + } + } + + public void stop() { + assert isRunningOnLauncherThread(); + Log.d(TAG, "stopping child connection: pid=%d", mConnection.getPid()); + mConnection.stop(); + } +} diff --git a/base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java new file mode 100644 index 0000000000..876ebbb175 --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessService.java @@ -0,0 +1,346 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.util.SparseArray; + +import org.chromium.base.BaseSwitches; +import org.chromium.base.CommandLine; +import org.chromium.base.ContextUtils; +import org.chromium.base.Log; +import org.chromium.base.MemoryPressureLevel; +import org.chromium.base.ThreadUtils; +import org.chromium.base.annotations.JNINamespace; +import org.chromium.base.annotations.MainDex; +import org.chromium.base.memory.MemoryPressureMonitor; + +import java.util.List; +import java.util.concurrent.Semaphore; + +import javax.annotation.concurrent.GuardedBy; + +/** + * This is the base class for child services; the embedding application should contain + * ProcessService0, 1.. etc subclasses that provide the concrete service entry points, so it can + * connect to more than one distinct process (i.e. one process per service number, up to limit of + * N). + * The embedding application must declare these service instances in the application section + * of its AndroidManifest.xml, first with some meta-data describing the services: + * <meta-data android:name="org.chromium.test_app.SERVICES_NAME" + * android:value="org.chromium.test_app.ProcessService"/> + * and then N entries of the form: + * <service android:name="org.chromium.test_app.ProcessServiceX" + * android:process=":processX" /> + * + * Subclasses must also provide a delegate in this class constructor. That delegate is responsible + * for loading native libraries and running the main entry point of the service. + */ +@JNINamespace("base::android") +@MainDex +public abstract class ChildProcessService extends Service { + private static final String MAIN_THREAD_NAME = "ChildProcessMain"; + private static final String TAG = "ChildProcessService"; + + // Only for a check that create is only called once. + private static boolean sCreateCalled; + + private final ChildProcessServiceDelegate mDelegate; + + private final Object mBinderLock = new Object(); + private final Object mLibraryInitializedLock = new Object(); + + // True if we should enforce that bindToCaller() is called before setupConnection(). + // Only set once in bind(), does not require synchronization. + private boolean mBindToCallerCheck; + + // PID of the client of this service, set in bindToCaller(), if mBindToCallerCheck is true. + @GuardedBy("mBinderLock") + private int mBoundCallingPid; + + // This is the native "Main" thread for the renderer / utility process. + private Thread mMainThread; + + // Parameters received via IPC, only accessed while holding the mMainThread monitor. + private String[] mCommandLineParams; + + // File descriptors that should be registered natively. + private FileDescriptorInfo[] mFdInfos; + + @GuardedBy("mLibraryInitializedLock") + private boolean mLibraryInitialized; + + // Called once the service is bound and all service related member variables have been set. + // Only set once in bind(), does not require synchronization. + private boolean mServiceBound; + + private final Semaphore mActivitySemaphore = new Semaphore(1); + + public ChildProcessService(ChildProcessServiceDelegate delegate) { + mDelegate = delegate; + } + + // Binder object used by clients for this service. + private final IChildProcessService.Stub mBinder = new IChildProcessService.Stub() { + // NOTE: Implement any IChildProcessService methods here. + @Override + public boolean bindToCaller() { + assert mBindToCallerCheck; + assert mServiceBound; + synchronized (mBinderLock) { + int callingPid = Binder.getCallingPid(); + if (mBoundCallingPid == 0) { + mBoundCallingPid = callingPid; + } else if (mBoundCallingPid != callingPid) { + Log.e(TAG, "Service is already bound by pid %d, cannot bind for pid %d", + mBoundCallingPid, callingPid); + return false; + } + } + return true; + } + + @Override + public void setupConnection(Bundle args, ICallbackInt pidCallback, List<IBinder> callbacks) + throws RemoteException { + assert mServiceBound; + synchronized (mBinderLock) { + if (mBindToCallerCheck && mBoundCallingPid == 0) { + Log.e(TAG, "Service has not been bound with bindToCaller()"); + pidCallback.call(-1); + return; + } + } + + pidCallback.call(Process.myPid()); + processConnectionBundle(args, callbacks); + } + + @Override + public void forceKill() { + assert mServiceBound; + Process.killProcess(Process.myPid()); + } + + @Override + public void onMemoryPressure(@MemoryPressureLevel int pressure) { + // This method is called by the host process when the host process reports pressure + // to its native side. The key difference between the host process and its services is + // that the host process polls memory pressure when it gets CRITICAL, and periodically + // invokes pressure listeners until pressure subsides. (See MemoryPressureMonitor for + // more info.) + // + // Services don't poll, so this side-channel is used to notify services about memory + // pressure from the host process's POV. + // + // However, since both host process and services listen to ComponentCallbacks2, we + // can't be sure that the host process won't get better signals than their services. + // I.e. we need to watch out for a situation where a service gets CRITICAL, but the + // host process gets MODERATE - in this case we need to ignore MODERATE. + // + // So we're ignoring pressure from the host process if it's better than the last + // reported pressure. I.e. the host process can drive pressure up, but it'll go + // down only when we the service get a signal through ComponentCallbacks2. + ThreadUtils.postOnUiThread(() -> { + if (pressure >= MemoryPressureMonitor.INSTANCE.getLastReportedPressure()) { + MemoryPressureMonitor.INSTANCE.notifyPressure(pressure); + } + }); + } + }; + + /** + * Loads Chrome's native libraries and initializes a ChildProcessService. + */ + // For sCreateCalled check. + @Override + public void onCreate() { + super.onCreate(); + Log.i(TAG, "Creating new ChildProcessService pid=%d", Process.myPid()); + if (sCreateCalled) { + throw new RuntimeException("Illegal child process reuse."); + } + sCreateCalled = true; + + // Initialize the context for the application that owns this ChildProcessService object. + ContextUtils.initApplicationContext(getApplicationContext()); + + mDelegate.onServiceCreated(); + + mMainThread = new Thread(new Runnable() { + @Override + public void run() { + try { + // CommandLine must be initialized before everything else. + synchronized (mMainThread) { + while (mCommandLineParams == null) { + mMainThread.wait(); + } + } + assert mServiceBound; + CommandLine.init(mCommandLineParams); + + if (CommandLine.getInstance().hasSwitch( + BaseSwitches.RENDERER_WAIT_FOR_JAVA_DEBUGGER)) { + android.os.Debug.waitForDebugger(); + } + + boolean nativeLibraryLoaded = false; + try { + nativeLibraryLoaded = mDelegate.loadNativeLibrary(getApplicationContext()); + } catch (Exception e) { + Log.e(TAG, "Failed to load native library.", e); + } + if (!nativeLibraryLoaded) { + System.exit(-1); + } + + synchronized (mLibraryInitializedLock) { + mLibraryInitialized = true; + mLibraryInitializedLock.notifyAll(); + } + synchronized (mMainThread) { + mMainThread.notifyAll(); + while (mFdInfos == null) { + mMainThread.wait(); + } + } + + SparseArray<String> idsToKeys = mDelegate.getFileDescriptorsIdsToKeys(); + + int[] fileIds = new int[mFdInfos.length]; + String[] keys = new String[mFdInfos.length]; + int[] fds = new int[mFdInfos.length]; + long[] regionOffsets = new long[mFdInfos.length]; + long[] regionSizes = new long[mFdInfos.length]; + for (int i = 0; i < mFdInfos.length; i++) { + FileDescriptorInfo fdInfo = mFdInfos[i]; + String key = idsToKeys != null ? idsToKeys.get(fdInfo.id) : null; + if (key != null) { + keys[i] = key; + } else { + fileIds[i] = fdInfo.id; + } + fds[i] = fdInfo.fd.detachFd(); + regionOffsets[i] = fdInfo.offset; + regionSizes[i] = fdInfo.size; + } + nativeRegisterFileDescriptors(keys, fileIds, fds, regionOffsets, regionSizes); + + mDelegate.onBeforeMain(); + if (mActivitySemaphore.tryAcquire()) { + mDelegate.runMain(); + nativeExitChildProcess(); + } + } catch (InterruptedException e) { + Log.w(TAG, "%s startup failed: %s", MAIN_THREAD_NAME, e); + } + } + }, MAIN_THREAD_NAME); + mMainThread.start(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.i(TAG, "Destroying ChildProcessService pid=%d", Process.myPid()); + if (mActivitySemaphore.tryAcquire()) { + // TODO(crbug.com/457406): This is a bit hacky, but there is no known better solution + // as this service will get reused (at least if not sandboxed). + // In fact, we might really want to always exit() from onDestroy(), not just from + // the early return here. + System.exit(0); + return; + } + synchronized (mLibraryInitializedLock) { + try { + while (!mLibraryInitialized) { + // Avoid a potential race in calling through to native code before the library + // has loaded. + mLibraryInitializedLock.wait(); + } + } catch (InterruptedException e) { + // Ignore + } + } + mDelegate.onDestroy(); + } + + /* + * Returns the communication channel to the service. Note that even if multiple clients were to + * connect, we should only get one call to this method. So there is no need to synchronize + * member variables that are only set in this method and accessed from binder methods, as binder + * methods can't be called until this method returns. + * @param intent The intent that was used to bind to the service. + * @return the binder used by the client to setup the connection. + */ + @Override + public IBinder onBind(Intent intent) { + assert !mServiceBound; + + // We call stopSelf() to request that this service be stopped as soon as the client unbinds. + // Otherwise the system may keep it around and available for a reconnect. The child + // processes do not currently support reconnect; they must be initialized from scratch every + // time. + stopSelf(); + + mBindToCallerCheck = + intent.getBooleanExtra(ChildProcessConstants.EXTRA_BIND_TO_CALLER, false); + mServiceBound = true; + mDelegate.onServiceBound(intent); + // Don't block bind() with any extra work, post it to the application thread instead. + new Handler(Looper.getMainLooper()) + .post(() -> mDelegate.preloadNativeLibrary(getApplicationContext())); + return mBinder; + } + + private void processConnectionBundle(Bundle bundle, List<IBinder> clientInterfaces) { + // Required to unparcel FileDescriptorInfo. + ClassLoader classLoader = getApplicationContext().getClassLoader(); + bundle.setClassLoader(classLoader); + synchronized (mMainThread) { + if (mCommandLineParams == null) { + mCommandLineParams = + bundle.getStringArray(ChildProcessConstants.EXTRA_COMMAND_LINE); + mMainThread.notifyAll(); + } + // We must have received the command line by now + assert mCommandLineParams != null; + Parcelable[] fdInfosAsParcelable = + bundle.getParcelableArray(ChildProcessConstants.EXTRA_FILES); + if (fdInfosAsParcelable != null) { + // For why this arraycopy is necessary: + // http://stackoverflow.com/questions/8745893/i-dont-get-why-this-classcastexception-occurs + mFdInfos = new FileDescriptorInfo[fdInfosAsParcelable.length]; + System.arraycopy(fdInfosAsParcelable, 0, mFdInfos, 0, fdInfosAsParcelable.length); + } + mDelegate.onConnectionSetup(bundle, clientInterfaces); + mMainThread.notifyAll(); + } + } + + /** + * Helper for registering FileDescriptorInfo objects with GlobalFileDescriptors or + * FileDescriptorStore. + * This includes the IPC channel, the crash dump signals and resource related + * files. + */ + private static native void nativeRegisterFileDescriptors( + String[] keys, int[] id, int[] fd, long[] offset, long[] size); + + /** + * Force the child process to exit. + */ + private static native void nativeExitChildProcess(); +} diff --git a/base/android/java/src/org/chromium/base/process_launcher/ChildProcessServiceDelegate.java b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessServiceDelegate.java new file mode 100644 index 0000000000..7beffeffa2 --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/ChildProcessServiceDelegate.java @@ -0,0 +1,76 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; +import android.util.SparseArray; + +import java.util.List; + +/** + * The interface that embedders should implement to specialize child service creation. + */ +public interface ChildProcessServiceDelegate { + /** Invoked when the service was created. This is the first method invoked on the delegate. */ + void onServiceCreated(); + + /** + * Called when the service is bound. Invoked on a background thread. + * @param intent the intent that started the service. + */ + void onServiceBound(Intent intent); + + /** + * Called once the connection has been setup. Invoked on a background thread. + * @param connectionBundle the bundle pass to the setupConnection call + * @param clientInterfaces the IBinders interfaces provided by the client + */ + void onConnectionSetup(Bundle connectionBundle, List<IBinder> clientInterfaces); + + /** + * Called when the service gets destroyed. + * Note that the system might kill the process hosting the service without this method being + * called. + */ + void onDestroy(); + + /** + * Called when the delegate should load the native library. + * @param hostContext The host context the library should be loaded with (i.e. Chrome). + * @return true if the library was loaded successfully, false otherwise in which case the + * service stops. + */ + boolean loadNativeLibrary(Context hostContext); + + /** + * Called when the delegate should preload the native library. + * Preloading is automatically done during library loading, but can also be called explicitly + * to speed up the loading. See {@link LibraryLoader.preloadNow}. + * @param hostContext The host context the library should be preloaded with (i.e. Chrome). + */ + void preloadNativeLibrary(Context hostContext); + + /** + * Should return a map that associatesfile descriptors' IDs to keys. + * This is needed as at the moment we use 2 different stores for the FDs in native code: + * base::FileDescriptorStore which associates FDs with string identifiers (the key), and + * base::GlobalDescriptors which associates FDs with int ids. + * FDs for which the returned map contains a mapping are added to base::FileDescriptorStore with + * the associated key, all others are added to base::GlobalDescriptors. + */ + SparseArray<String> getFileDescriptorsIdsToKeys(); + + /** Called before the main method is invoked. */ + void onBeforeMain(); + + /** + * The main entry point for the service. This method should block as long as the service should + * be running. + */ + void runMain(); +} diff --git a/base/android/java/src/org/chromium/base/process_launcher/FileDescriptorInfo.aidl b/base/android/java/src/org/chromium/base/process_launcher/FileDescriptorInfo.aidl new file mode 100644 index 0000000000..e37d8c748d --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/FileDescriptorInfo.aidl @@ -0,0 +1,7 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +parcelable FileDescriptorInfo; diff --git a/base/android/java/src/org/chromium/base/process_launcher/FileDescriptorInfo.java b/base/android/java/src/org/chromium/base/process_launcher/FileDescriptorInfo.java new file mode 100644 index 0000000000..3dc366389a --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/FileDescriptorInfo.java @@ -0,0 +1,68 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.Parcelable; + +import org.chromium.base.annotations.MainDex; +import org.chromium.base.annotations.UsedByReflection; + +import javax.annotation.concurrent.Immutable; + +/** + * Parcelable class that contains file descriptor and file region information to + * be passed to child processes. + */ +@Immutable +@MainDex +@UsedByReflection("child_process_launcher_helper_android.cc") +public final class FileDescriptorInfo implements Parcelable { + public final int id; + public final ParcelFileDescriptor fd; + public final long offset; + public final long size; + + public FileDescriptorInfo(int id, ParcelFileDescriptor fd, long offset, long size) { + this.id = id; + this.fd = fd; + this.offset = offset; + this.size = size; + } + + FileDescriptorInfo(Parcel in) { + id = in.readInt(); + fd = in.readParcelable(ParcelFileDescriptor.class.getClassLoader()); + offset = in.readLong(); + size = in.readLong(); + } + + @Override + public int describeContents() { + return CONTENTS_FILE_DESCRIPTOR; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(id); + dest.writeParcelable(fd, CONTENTS_FILE_DESCRIPTOR); + dest.writeLong(offset); + dest.writeLong(size); + } + + public static final Parcelable.Creator<FileDescriptorInfo> CREATOR = + new Parcelable.Creator<FileDescriptorInfo>() { + @Override + public FileDescriptorInfo createFromParcel(Parcel in) { + return new FileDescriptorInfo(in); + } + + @Override + public FileDescriptorInfo[] newArray(int size) { + return new FileDescriptorInfo[size]; + } + }; +} diff --git a/base/android/java/src/org/chromium/base/process_launcher/ICallbackInt.aidl b/base/android/java/src/org/chromium/base/process_launcher/ICallbackInt.aidl new file mode 100644 index 0000000000..db93cb0dbd --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/ICallbackInt.aidl @@ -0,0 +1,9 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +oneway interface ICallbackInt { + void call(int value); +} diff --git a/base/android/java/src/org/chromium/base/process_launcher/IChildProcessService.aidl b/base/android/java/src/org/chromium/base/process_launcher/IChildProcessService.aidl new file mode 100644 index 0000000000..298e0bf49a --- /dev/null +++ b/base/android/java/src/org/chromium/base/process_launcher/IChildProcessService.aidl @@ -0,0 +1,26 @@ +// Copyright 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.process_launcher; + +import android.os.Bundle; + +import org.chromium.base.process_launcher.ICallbackInt; + +interface IChildProcessService { + // On the first call to this method, the service will record the calling PID + // and return true. Subsequent calls will only return true if the calling PID + // is the same as the recorded one. + boolean bindToCaller(); + + // Sets up the initial IPC channel. + oneway void setupConnection(in Bundle args, ICallbackInt pidCallback, + in List<IBinder> clientInterfaces); + + // Forcefully kills the child process. + oneway void forceKill(); + + // Notifies about memory pressure. The argument is MemoryPressureLevel enum. + oneway void onMemoryPressure(int pressure); +} diff --git a/base/android/java/templates/BuildConfig.template b/base/android/java/templates/BuildConfig.template new file mode 100644 index 0000000000..1006d12ee1 --- /dev/null +++ b/base/android/java/templates/BuildConfig.template @@ -0,0 +1,70 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base; + +#define Q(x) #x +#define QUOTE(x) Q(x) + +#if defined(USE_FINAL) +#define MAYBE_FINAL final +#else +#define MAYBE_FINAL +#endif + +/** + * Build configuration. Generated on a per-target basis. + */ +public class BuildConfig { + +#if defined(ENABLE_MULTIDEX) + public static MAYBE_FINAL boolean IS_MULTIDEX_ENABLED = true; +#else + public static MAYBE_FINAL boolean IS_MULTIDEX_ENABLED = false; +#endif + +#if defined(_FIREBASE_APP_ID) + public static MAYBE_FINAL String FIREBASE_APP_ID = QUOTE(_FIREBASE_APP_ID); +#else + public static MAYBE_FINAL String FIREBASE_APP_ID = ""; +#endif + +#if defined(_DCHECK_IS_ON) + public static MAYBE_FINAL boolean DCHECK_IS_ON = true; +#else + public static MAYBE_FINAL boolean DCHECK_IS_ON = false; +#endif + +#if defined(_IS_UBSAN) + public static MAYBE_FINAL boolean IS_UBSAN = true; +#else + public static MAYBE_FINAL boolean IS_UBSAN = false; +#endif + + // Sorted list of locales that have a compressed .pak within assets. + // Stored as an array because AssetManager.list() is slow. +#if defined(COMPRESSED_LOCALE_LIST) + public static MAYBE_FINAL String[] COMPRESSED_LOCALES = COMPRESSED_LOCALE_LIST; +#else + public static MAYBE_FINAL String[] COMPRESSED_LOCALES = {}; +#endif + + // Sorted list of locales that have an uncompressed .pak within assets. + // Stored as an array because AssetManager.list() is slow. +#if defined(UNCOMPRESSED_LOCALE_LIST) + public static MAYBE_FINAL String[] UNCOMPRESSED_LOCALES = UNCOMPRESSED_LOCALE_LIST; +#else + public static MAYBE_FINAL String[] UNCOMPRESSED_LOCALES = {}; +#endif + + // The ID of the android string resource that stores the product version. + // This layer of indirection is necessary to make the resource dependency + // optional for android_apk targets/base_java (ex. for cronet). +#if defined(_RESOURCES_VERSION_VARIABLE) + public static MAYBE_FINAL int R_STRING_PRODUCT_VERSION = _RESOURCES_VERSION_VARIABLE; +#else + // Default value, do not use. + public static MAYBE_FINAL int R_STRING_PRODUCT_VERSION = 0; +#endif +} diff --git a/base/android/java/templates/NativeLibraries.template b/base/android/java/templates/NativeLibraries.template new file mode 100644 index 0000000000..68277df753 --- /dev/null +++ b/base/android/java/templates/NativeLibraries.template @@ -0,0 +1,109 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.base.library_loader; + +public class NativeLibraries { + /** + * IMPORTANT NOTE: The variables defined here must _not_ be 'final'. + * + * The reason for this is very subtle: + * + * - This template is used to generate several distinct, but similar + * files used in different contexts: + * + * o .../gen/templates/org/chromium/base/library_loader/NativeLibraries.java + * + * This file is used to build base.jar, which is the library + * jar used by chromium projects. However, the + * corresponding NativeLibraries.class file will _not_ be part + * of the final base.jar. + * + * o .../$PROJECT/native_libraries_java/NativeLibraries.java + * + * This file is used to build an APK (e.g. $PROJECT + * could be 'content_shell_apk'). Its content will depend on + * this target's specific build configuration, and differ from + * the source file above. + * + * - During the final link, all .jar files are linked together into + * a single .dex file, and the second version of NativeLibraries.class + * will be put into the final output file, and used at runtime. + * + * - If the variables were defined as 'final', their value would be + * optimized out inside of 'base.jar', and could not be specialized + * for every chromium program. This, however, doesn't apply to arrays of + * strings, which can be defined as final. + * + * This exotic scheme is used to avoid injecting project-specific, or + * even build-specific, values into the base layer. E.g. this is + * how the component build is supported on Android without modifying + * the sources of each and every Chromium-based target. + */ + + public static final int CPU_FAMILY_UNKNOWN = 0; + public static final int CPU_FAMILY_ARM = 1; + public static final int CPU_FAMILY_MIPS = 2; + public static final int CPU_FAMILY_X86 = 3; + +#if defined(ENABLE_CHROMIUM_LINKER_LIBRARY_IN_ZIP_FILE) && \ + !defined(ENABLE_CHROMIUM_LINKER) +#error "Must have ENABLE_CHROMIUM_LINKER to enable library in zip file" +#endif + + // Set to true to enable the use of the Chromium Linker. +#if defined(ENABLE_CHROMIUM_LINKER) + public static boolean sUseLinker = true; +#else + public static boolean sUseLinker = false; +#endif + +#if defined(ENABLE_CHROMIUM_LINKER_LIBRARY_IN_ZIP_FILE) + public static boolean sUseLibraryInZipFile = true; +#else + public static boolean sUseLibraryInZipFile = false; +#endif + +#if defined(ENABLE_CHROMIUM_LINKER_TESTS) + public static boolean sEnableLinkerTests = true; +#else + public static boolean sEnableLinkerTests = false; +#endif + + // This is the list of native libraries to be loaded (in the correct order) + // by LibraryLoader.java. The base java library is compiled with no + // array defined, and then the build system creates a version of the file + // with the real list of libraries required (which changes based upon which + // .apk is being built). + // TODO(cjhopman): This is public since it is referenced by NativeTestActivity.java + // directly. The two ways of library loading should be refactored into one. + public static final String[] LIBRARIES = +#if defined(NATIVE_LIBRARIES_LIST) + NATIVE_LIBRARIES_LIST; +#else + {}; +#endif + + // This is the expected version of the 'main' native library, which is the one that + // implements the initial set of base JNI functions including + // base::android::nativeGetVersionName() + static String sVersionNumber = +#if defined(NATIVE_LIBRARIES_VERSION_NUMBER) + NATIVE_LIBRARIES_VERSION_NUMBER; +#else + ""; +#endif + + public static int sCpuFamily = +#if defined(ANDROID_APP_CPU_FAMILY_ARM) + CPU_FAMILY_ARM; +#elif defined(ANDROID_APP_CPU_FAMILY_X86) + CPU_FAMILY_X86; +#elif defined(ANDROID_APP_CPU_FAMILY_MIPS) + CPU_FAMILY_MIPS; +#else + CPU_FAMILY_UNKNOWN; +#endif + +} |