// 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.chrome.browser.superviseduser; import android.annotation.TargetApi; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; import android.os.SystemClock; import android.os.UserManager; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import org.chromium.base.annotations.CalledByNative; import org.chromium.base.library_loader.LibraryLoader; import org.chromium.base.library_loader.ProcessInitException; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.browser.init.ChromeBrowserInitializer; import org.chromium.components.webrestrictions.browser.WebRestrictionsContentProvider; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; /** * Content provider for telling other apps (e.g. WebView apps) about the supervised user URL filter. */ public class SupervisedUserContentProvider extends WebRestrictionsContentProvider { private static final String SUPERVISED_USER_CONTENT_PROVIDER_ENABLED = "SupervisedUserContentProviderEnabled"; private long mNativeSupervisedUserContentProvider = 0; private boolean mChromeAlreadyStarted; private static Object sEnabledLock = new Object(); // Three value "boolean" caching enabled state, null if not yet known. private static Boolean sEnabled = null; private long getSupervisedUserContentProvider() throws ProcessInitException { mChromeAlreadyStarted = LibraryLoader.isInitialized(); if (mNativeSupervisedUserContentProvider != 0) { return mNativeSupervisedUserContentProvider; } ChromeBrowserInitializer.getInstance(getContext()).handleSynchronousStartup(); mNativeSupervisedUserContentProvider = nativeCreateSupervisedUserContentProvider(); return mNativeSupervisedUserContentProvider; } void setNativeSupervisedUserContentProviderForTesting(long nativeProvider) { mNativeSupervisedUserContentProvider = nativeProvider; } static class SupervisedUserReply<T> { private static final long RESULT_TIMEOUT_SECONDS = 10; final BlockingQueue<T> mQueue = new ArrayBlockingQueue<>(1); void onQueryFinished(T reply) { // This must be called precisely once per query. mQueue.add(reply); } T getResult() throws InterruptedException { return mQueue.poll(RESULT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } } static class SupervisedUserQueryReply extends SupervisedUserReply<WebRestrictionsResult> { // One of the following three functions must be called precisely once per query. @CalledByNative("SupervisedUserQueryReply") void onQueryComplete() { onQueryFinished(new WebRestrictionsResult(true, null, null)); } @CalledByNative("SupervisedUserQueryReply") void onQueryFailed(int reason, int allowAccessRequests, int isChildAccount, String profileImageUrl, String profileImageUrl2, String custodian, String custodianEmail, String secondCustodian, String secondCustodianEmail) { int errorInt[] = new int[] {reason, allowAccessRequests, isChildAccount}; String errorString[] = new String[] { profileImageUrl, profileImageUrl2, custodian, custodianEmail, secondCustodian, secondCustodianEmail }; onQueryFinished(new WebRestrictionsResult(false, errorInt, errorString)); } void onQueryFailedNoErrorData() { onQueryFinished(new WebRestrictionsResult(false, null, null)); } } @Override protected WebRestrictionsResult shouldProceed(final String url) { // This will be called on multiple threads (but never the UI thread), // see http://developer.android.com/guide/components/processes-and-threads.html#ThreadSafe. // The reply comes back on a different thread (possibly the UI thread) some time later. // As such it needs to correctly match the replies to the calls. It does this by creating a // reply object for each query, and passing this through the callback structure. The reply // object also handles waiting for the reply. long startTimeMs = SystemClock.elapsedRealtime(); final SupervisedUserQueryReply queryReply = new SupervisedUserQueryReply(); ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { try { nativeShouldProceed(getSupervisedUserContentProvider(), queryReply, url); } catch (ProcessInitException e) { queryReply.onQueryFailedNoErrorData(); } } }); try { // This will block until an onQueryComplete call on a different thread adds // something to the queue. WebRestrictionsResult result = queryReply.getResult(); String histogramName = mChromeAlreadyStarted ? "SupervisedUserContentProvider.ChromeStartedRequestTime" : "SupervisedUserContentProvider.ChromeNotStartedRequestTime"; RecordHistogram.recordTimesHistogram(histogramName, SystemClock.elapsedRealtime() - startTimeMs, TimeUnit.MILLISECONDS); RecordHistogram.recordBooleanHistogram( "SupervisedUserContentProvider.RequestTimedOut", result == null); if (result == null) return new WebRestrictionsResult(false, null, null); return result; } catch (InterruptedException e) { return new WebRestrictionsResult(false, null, null); } } @Override protected boolean canInsert() { // Chrome always allows insertion requests. return true; } static class SupervisedUserInsertReply extends SupervisedUserReply<Boolean> { @CalledByNative("SupervisedUserInsertReply") void onInsertRequestSendComplete(boolean result) { onQueryFinished(result); } } @Override protected boolean requestInsert(final String url) { // This will be called on multiple threads (but never the UI thread), // see http://developer.android.com/guide/components/processes-and-threads.html#ThreadSafe. // The reply comes back on a different thread (possibly the UI thread) some time later. // As such it needs to correctly match the replies to the calls. It does this by creating a // reply object for each query, and passing this through the callback structure. The reply // object also handles waiting for the reply. final SupervisedUserInsertReply insertReply = new SupervisedUserInsertReply(); ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { try { nativeRequestInsert(getSupervisedUserContentProvider(), insertReply, url); } catch (ProcessInitException e) { insertReply.onInsertRequestSendComplete(false); } } }); try { Boolean result = insertReply.getResult(); if (result == null) return false; return result; } catch (InterruptedException e) { return false; } } @Override public Bundle call(String method, String arg, Bundle bundle) { if (method.equals("setFilterForTesting")) setFilterForTesting(); return null; } void setFilterForTesting() { ThreadUtils.runOnUiThreadBlocking(new Runnable() { @Override public void run() { try { nativeSetFilterForTesting(getSupervisedUserContentProvider()); } catch (ProcessInitException e) { // There is no way of returning anything sensible here, so ignore the error and // do nothing. } } }); } @CalledByNative void onSupervisedUserFilterUpdated() { onFilterChanged(); } private static Boolean getEnabled() { synchronized (sEnabledLock) { return sEnabled; } } private static void setEnabled(boolean enabled) { synchronized (sEnabledLock) { sEnabled = enabled; } } @Override protected boolean contentProviderEnabled() { if (getEnabled() != null) return getEnabled(); // There wasn't a fully functional App Restrictions system in Android (including the // broadcast intent for updates) until Lollipop. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return false; updateEnabledState(); createEnabledBroadcastReceiver(); return getEnabled(); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void createEnabledBroadcastReceiver() { IntentFilter restrictionsFilter = new IntentFilter( Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED); BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { updateEnabledState(); } }; getContext().registerReceiver(restrictionsReceiver, restrictionsFilter); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) private void updateEnabledState() { // This method uses AppRestrictions directly, rather than using the Policy interface, // because it must be callable in contexts in which the native library hasn't been // loaded. It will always be called from a background thread (except possibly in tests) // so can get the App Restrictions synchronously. UserManager userManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE); Bundle appRestrictions = userManager .getApplicationRestrictions(getContext().getPackageName()); setEnabled(appRestrictions.getBoolean(SUPERVISED_USER_CONTENT_PROVIDER_ENABLED)); }; @VisibleForTesting public static void enableContentProviderForTesting() { setEnabled(true); } native long nativeCreateSupervisedUserContentProvider(); native void nativeShouldProceed(long nativeSupervisedUserContentProvider, SupervisedUserQueryReply queryReply, String url); native void nativeRequestInsert(long nativeSupervisedUserContentProvider, SupervisedUserInsertReply insertReply, String url); private native void nativeSetFilterForTesting(long nativeSupervisedUserContentProvider); @Override protected String[] getErrorColumnNames() { String result[] = {"Reason", "Allow access requests", "Is child account", "Profile image URL", "Second profile image URL", "Custodian", "Custodian email", "Second custodian", "Second custodian email"}; return result; } }