// 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.ContentResolver;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import org.chromium.base.CommandLine;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.partnerbookmarks.PartnerBookmarksReader;
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 = "PartnerBrowserProvider";
private static final String PROVIDER_AUTHORITY = "com.android.partnerbrowsercustomizations";
// Private homepage structure.
static final String PARTNER_HOMEPAGE_PATH = "homepage";
static final String PARTNER_DISABLE_BOOKMARKS_EDITING_PATH = "disablebookmarksediting";
@VisibleForTesting
public static final String PARTNER_DISABLE_INCOGNITO_MODE_PATH = "disableincognitomode";
private static String sProviderAuthority = PROVIDER_AUTHORITY;
private static boolean sIgnoreBrowserProviderSystemPackageCheck = false;
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<Runnable>();
/**
* @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.
*/
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
public 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
public static void ignoreBrowserProviderSystemPackageCheckForTests(boolean ignore) {
sIgnoreBrowserProviderSystemPackageCheck = ignore;
}
@VisibleForTesting
public static Uri buildQueryUri(String path) {
return new Uri.Builder()
.scheme("content")
.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;
// Setup an initializing async task.
final AsyncTask<Void, Void, Void> initializeAsyncTask =
new AsyncTask<Void, Void, Void>() {
private boolean mDisablePartnerBookmarksShim;
private boolean mHomepageUriChanged;
private void refreshHomepage() {
try {
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(
buildQueryUri(PARTNER_HOMEPAGE_PATH), null, null, null, null);
if (cursor != null && cursor.moveToFirst() && cursor.getColumnCount() == 1
&& !isCancelled()) {
if (TextUtils.isEmpty(sHomepage)
|| !sHomepage.equals(cursor.getString(0))) {
mHomepageUriChanged = true;
}
sHomepage = cursor.getString(0);
}
if (cursor != null) cursor.close();
} catch (Exception e) {
Log.w(TAG, "Partner homepage provider URL read failed : ", e);
}
}
private void refreshIncognitoModeDisabled() {
try {
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(
buildQueryUri(PARTNER_DISABLE_INCOGNITO_MODE_PATH),
null, null, null, null);
if (cursor != null && cursor.moveToFirst() && cursor.getColumnCount() == 1
&& !isCancelled()) {
sIncognitoModeDisabled = cursor.getInt(0) == 1;
}
if (cursor != null) cursor.close();
} catch (Exception e) {
Log.w(TAG, "Partner disable incognito mode read failed : ", e);
}
}
private void refreshBookmarksEditingDisabled() {
try {
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(
buildQueryUri(PARTNER_DISABLE_BOOKMARKS_EDITING_PATH),
null, null, null, null);
if (cursor != null && cursor.moveToFirst() && cursor.getColumnCount() == 1
&& !isCancelled()) {
boolean bookmarksEditingDisabled = cursor.getInt(0) == 1;
if (bookmarksEditingDisabled != sBookmarksEditingDisabled) {
mDisablePartnerBookmarksShim = true;
}
sBookmarksEditingDisabled = bookmarksEditingDisabled;
}
if (cursor != null) cursor.close();
} catch (Exception e) {
Log.w(TAG, "Partner disable bookmarks editing read failed : ", e);
}
}
@Override
protected Void doInBackground(Void... params) {
try {
ProviderInfo providerInfo = context.getPackageManager()
.resolveContentProvider(sProviderAuthority, 0);
if (providerInfo == null) return null;
if ((providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
&& !sIgnoreBrowserProviderSystemPackageCheck) {
Log.w("TAG", "Browser Cutomizations content provider package, "
+ providerInfo.packageName + ", is not a system package. "
+ "This could be a malicious attepment from a third party app, "
+ "so skip reading the browser content provider.");
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(context).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(new Runnable() {
@Override
public void run() {
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) {
ThreadUtils.postOnUiThread(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(
new Runnable() {
@Override
public void run() {
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;
}
}