| // 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; |
| } |
| } |