blob: 04958d1186cd6b67b29064a0b6ee96a276b981d5 [file] [log] [blame]
// 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.partnercustomizations;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.ChromeVersionInfo;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.partnerbookmarks.PartnerBookmarksReader;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import java.util.ArrayList;
import java.util.List;
/**
* Reads and caches partner browser customizations information if it exists.
*/
public class PartnerBrowserCustomizations {
private static final String TAG = "PartnerCustomize";
private static final String PROVIDER_AUTHORITY = "com.android.partnerbrowsercustomizations";
private static final int HOMEPAGE_URL_MAX_LENGTH = 1000;
// Private homepage structure.
@VisibleForTesting
static final String PARTNER_HOMEPAGE_PATH = "homepage";
@VisibleForTesting
static final String PARTNER_DISABLE_BOOKMARKS_EDITING_PATH = "disablebookmarksediting";
@VisibleForTesting
static final String PARTNER_DISABLE_INCOGNITO_MODE_PATH = "disableincognitomode";
private static String sProviderAuthority = PROVIDER_AUTHORITY;
private static boolean sIgnoreBrowserProviderSystemPackageCheck;
private static volatile String sHomepage;
private static volatile boolean sIncognitoModeDisabled;
private static volatile boolean sBookmarksEditingDisabled;
private static boolean sIsInitialized;
private static List<Runnable> sInitializeAsyncCallbacks = new ArrayList<>();
/** Provider of partner customizations. */
public interface Provider {
@Nullable
String getHomepage();
boolean isIncognitoModeDisabled();
boolean isBookmarksEditingDisabled();
}
/** Partner customizations provided by ContentProvider package. */
public static class ProviderPackage implements Provider {
private static Boolean sValid;
private boolean isValidInternal() {
ProviderInfo providerInfo =
ContextUtils.getApplicationContext().getPackageManager().resolveContentProvider(
sProviderAuthority, 0);
if (providerInfo == null) return false;
if ((providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
&& !sIgnoreBrowserProviderSystemPackageCheck) {
Log.w(TAG,
"Browser Customizations content provider package, "
+ providerInfo.packageName + ", is not a system package. "
+ "This could be a malicious attempt from a third party "
+ "app, so skip reading the browser content provider.");
return false;
}
return true;
}
private boolean isValid() {
if (sValid == null) sValid = isValidInternal();
return sValid;
}
@Override
public String getHomepage() {
if (!isValid()) return null;
String homepage = null;
Cursor cursor = ContextUtils.getApplicationContext().getContentResolver().query(
buildQueryUri(PARTNER_HOMEPAGE_PATH), null, null, null, null);
if (cursor != null && cursor.moveToFirst() && cursor.getColumnCount() == 1) {
homepage = cursor.getString(0);
}
if (cursor != null) cursor.close();
return homepage;
}
@Override
public boolean isIncognitoModeDisabled() {
if (!isValid()) return false;
boolean disabled = false;
Cursor cursor = ContextUtils.getApplicationContext().getContentResolver().query(
buildQueryUri(PARTNER_DISABLE_INCOGNITO_MODE_PATH), null, null, null, null);
if (cursor != null && cursor.moveToFirst() && cursor.getColumnCount() == 1) {
disabled = cursor.getInt(0) == 1;
}
if (cursor != null) cursor.close();
return disabled;
}
@Override
public boolean isBookmarksEditingDisabled() {
if (!isValid()) return false;
boolean disabled = false;
Cursor cursor = ContextUtils.getApplicationContext().getContentResolver().query(
buildQueryUri(PARTNER_DISABLE_BOOKMARKS_EDITING_PATH), null, null, null, null);
if (cursor != null && cursor.moveToFirst() && cursor.getColumnCount() == 1) {
disabled = cursor.getInt(0) == 1;
}
if (cursor != null) cursor.close();
return disabled;
}
}
/**
* @return True if the partner homepage content provider exists and enabled. Note that The data
* this method reads is not initialized until the asynchronous initialization of this
* class has been completed.
*/
public static boolean isHomepageProviderAvailableAndEnabled() {
return !TextUtils.isEmpty(getHomePageUrl());
}
/**
* @return Whether incognito mode is disabled by the partner.
*/
@CalledByNative
public static boolean isIncognitoDisabled() {
return sIncognitoModeDisabled;
}
/**
* @return Whether partner bookmarks editing is disabled by the partner.
*/
@VisibleForTesting
static boolean isBookmarksEditingDisabled() {
return sBookmarksEditingDisabled;
}
/**
* @return True, if initialization is finished. Checking that there is no provider, or failing
* to read provider is also considered initialization.
*/
@VisibleForTesting
public static boolean isInitialized() {
return sIsInitialized;
}
@VisibleForTesting
static void setProviderAuthorityForTests(String providerAuthority) {
sProviderAuthority = providerAuthority;
}
/**
* For security, we only allow system package to be a browser customizations provider. However,
* requiring root and installing system apk makes testing harder, so we decided to have this
* hack for testing. This must not be called other than tests.
*
* @param ignore whether we should ignore browser provider system package checking.
*/
@VisibleForTesting
static void ignoreBrowserProviderSystemPackageCheckForTests(boolean ignore) {
sIgnoreBrowserProviderSystemPackageCheck = ignore;
}
@VisibleForTesting
static Uri buildQueryUri(String path) {
return new Uri.Builder()
.scheme(UrlConstants.CONTENT_SCHEME)
.authority(sProviderAuthority)
.appendPath(path)
.build();
}
/**
* Constructs an async task that reads PartnerBrowserCustomization provider.
*
* @param context The current application context.
* @param timeoutMs If initializing takes more than this time, cancels it. The unit is ms.
*/
public static void initializeAsync(final Context context, long timeoutMs) {
sIsInitialized = false;
Provider provider = AppHooks.get().getCustomizationProvider();
// Setup an initializing async task.
final AsyncTask<Void> initializeAsyncTask = new AsyncTask<Void>() {
private boolean mDisablePartnerBookmarksShim;
private boolean mHomepageUriChanged;
private void refreshHomepage() {
try {
String homepage = provider.getHomepage();
if (!isValidHomepage(homepage)) homepage = null;
if (!TextUtils.equals(sHomepage, homepage)) {
mHomepageUriChanged = true;
}
sHomepage = homepage;
} catch (Exception e) {
Log.w(TAG, "Partner homepage provider URL read failed : ", e);
}
}
private void refreshIncognitoModeDisabled() {
try {
sIncognitoModeDisabled = provider.isIncognitoModeDisabled();
} catch (Exception e) {
Log.w(TAG, "Partner disable incognito mode read failed : ", e);
}
}
private void refreshBookmarksEditingDisabled() {
try {
boolean disabled = provider.isBookmarksEditingDisabled();
// Only need to disable it once.
if (disabled != sBookmarksEditingDisabled) {
assert disabled;
mDisablePartnerBookmarksShim = true;
}
sBookmarksEditingDisabled = disabled;
} catch (Exception e) {
Log.w(TAG, "Partner disable bookmarks editing read failed : ", e);
}
}
@Override
protected Void doInBackground() {
try {
boolean systemOrPreStable =
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) == 1
|| !ChromeVersionInfo.isStableBuild();
if (!systemOrPreStable) {
// Only allow partner customization if this browser is a system package, or
// is in pre-stable channels.
return null;
}
if (isCancelled()) return null;
refreshIncognitoModeDisabled();
if (isCancelled()) return null;
refreshBookmarksEditingDisabled();
if (isCancelled()) return null;
refreshHomepage();
} catch (Exception e) {
Log.w(TAG, "Fetching partner customizations failed", e);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
onFinalized();
}
@Override
protected void onCancelled(Void result) {
onFinalized();
}
private void onFinalized() {
sIsInitialized = true;
for (Runnable callback : sInitializeAsyncCallbacks) {
callback.run();
}
sInitializeAsyncCallbacks.clear();
if (mHomepageUriChanged) {
HomepageManager.getInstance().notifyHomepageUpdated();
}
// Disable partner bookmarks editing if necessary.
if (mDisablePartnerBookmarksShim) {
PartnerBookmarksReader.disablePartnerBookmarksEditing();
}
}
};
initializeAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
// Cancel the initialization if it reaches timeout.
ThreadUtils.postOnUiThreadDelayed(() -> initializeAsyncTask.cancel(true), timeoutMs);
}
/**
* Sets a callback that will be executed when the initialization is done.
*
* @param callback This is called when the initialization is done.
*/
public static void setOnInitializeAsyncFinished(final Runnable callback) {
if (sIsInitialized) {
PostTask.postTask(UiThreadTaskTraits.DEFAULT, callback);
} else {
sInitializeAsyncCallbacks.add(callback);
}
}
/**
* Sets a callback that will be executed when the initialization is done.
*
* @param callback This is called when the initialization is done.
* @param timeoutMs If initializing takes more than this time since this function is called,
* force run |callback| early. The unit is ms.
*/
public static void setOnInitializeAsyncFinished(final Runnable callback, long timeoutMs) {
sInitializeAsyncCallbacks.add(callback);
ThreadUtils.postOnUiThreadDelayed(() -> {
if (sInitializeAsyncCallbacks.remove(callback)) callback.run();
}, sIsInitialized ? 0 : timeoutMs);
}
public static void destroy() {
sIsInitialized = false;
sInitializeAsyncCallbacks.clear();
sHomepage = null;
}
/**
* @return Home page URL from Android provider. If null, that means either there is no homepage
* provider or provider set it to null to disable homepage.
*/
public static String getHomePageUrl() {
CommandLine commandLine = CommandLine.getInstance();
if (commandLine.hasSwitch(ChromeSwitches.PARTNER_HOMEPAGE_FOR_TESTING)) {
return commandLine.getSwitchValue(ChromeSwitches.PARTNER_HOMEPAGE_FOR_TESTING);
}
return sHomepage;
}
@VisibleForTesting
static boolean isValidHomepage(String url) {
if (url == null) return false;
if (!UrlUtilities.isHttpOrHttps(url) && !NewTabPage.isNTPUrl(url)) {
Log.w(TAG,
"Partner homepage must be HTTP(S) or NewTabPage. "
+ "Got invalid URL \"%s\"",
url);
return false;
}
if (url.length() > HOMEPAGE_URL_MAX_LENGTH) {
Log.w(TAG, "The homepage URL \"%s\" is too long.", url);
return false;
}
return true;
}
}