diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 26a232611d2..66988147104 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2005,6 +2005,9 @@ + + - + @@ -2950,7 +2955,7 @@ - + On + + Uninstalled for user %s\n + + Disabled for user %s\n + diff --git a/res/xml/development_prefs.xml b/res/xml/development_prefs.xml index 3c2bab9b78b..22327c3b1c4 100644 --- a/res/xml/development_prefs.xml +++ b/res/xml/development_prefs.xml @@ -74,11 +74,9 @@ android:title="@string/picture_color_mode" android:summary="@string/picture_color_mode_desc" /> - + android:dialogTitle="@string/select_webview_provider_dialog_title" /> options = new ArrayList(); - ArrayList values = new ArrayList(); - for (int n = 0; n < providers.length; n++) { - if (Utils.isPackageEnabled(getActivity(), providers[n].packageName)) { - options.add(providers[n].description); - values.add(providers[n].packageName); - } - } - mWebViewProvider.setEntries(options.toArray(new String[options.size()])); - mWebViewProvider.setEntryValues(values.toArray(new String[values.size()])); - - String value = mWebViewUpdateService.getCurrentWebViewPackageName(); - if (value == null) { - value = ""; - } - - for (int i = 0; i < values.size(); i++) { - if (value.contentEquals(values.get(i))) { - mWebViewProvider.setValueIndex(i); - return; - } - } - } catch (RemoteException e) { - } - } - private void updateWebViewMultiprocessOptions() { try { updateSwitchPreference(mWebViewMultiprocess, @@ -918,17 +886,6 @@ public class DevelopmentSettings extends RestrictedSettingsFragment mBtHciSnoopLog.isChecked() ? 1 : 0); } - private boolean writeWebViewProviderOptions(Object newValue) { - try { - String updatedProvider = mWebViewUpdateService.changeProviderAndSetting( - newValue == null ? "" : newValue.toString()); - updateWebViewProviderOptions(); - return newValue != null && newValue.equals(updatedProvider); - } catch (RemoteException e) { - } - return false; - } - private void writeDebuggerOptions() { try { ActivityManager.getService().setDebugApp( @@ -2327,6 +2284,8 @@ public class DevelopmentSettings extends RestrictedSettingsFragment writeMockLocation(); updateMockLocation(); } + } else if (requestCode == RESULT_WEBVIEW_APP) { + mWebViewAppPrefController.onActivityResult(resultCode, data); } else if (requestCode == REQUEST_CODE_ENABLE_OEM_UNLOCK) { if (resultCode == Activity.RESULT_OK) { if (mEnableOemUnlock.isChecked()) { @@ -2349,6 +2308,10 @@ public class DevelopmentSettings extends RestrictedSettingsFragment if (mBugReportInPowerController.handlePreferenceTreeClick(preference)) { return true; } + if (mWebViewAppPrefController.handlePreferenceTreeClick(preference)) { + startActivityForResult( + mWebViewAppPrefController.getActivityIntent(), RESULT_WEBVIEW_APP); + } if (preference == mEnableAdb) { if (mEnableAdb.isChecked()) { @@ -2502,21 +2465,6 @@ public class DevelopmentSettings extends RestrictedSettingsFragment updateHdcpValues(); pokeSystemProperties(); return true; - } else if (preference == mWebViewProvider) { - if (newValue == null) { - Log.e(TAG, "Tried to set a null WebView provider"); - return false; - } - if (writeWebViewProviderOptions(newValue)) { - return true; - } else { - // The user chose a package that became invalid since the list was last updated, - // show a Toast to explain the situation. - Toast toast = Toast.makeText(getActivity(), - R.string.select_webview_provider_toast_text, Toast.LENGTH_SHORT); - toast.show(); - } - return false; } else if ((preference == mBluetoothSelectA2dpCodec) || (preference == mBluetoothSelectA2dpSampleRate) || (preference == mBluetoothSelectA2dpBitsPerSample) || diff --git a/src/com/android/settings/SettingsInitialize.java b/src/com/android/settings/SettingsInitialize.java index 07fec0741ab..66fc4d690b9 100644 --- a/src/com/android/settings/SettingsInitialize.java +++ b/src/com/android/settings/SettingsInitialize.java @@ -44,6 +44,8 @@ public class SettingsInitialize extends BroadcastReceiver { private static final String TAG = "Settings"; private static final String PRIMARY_PROFILE_SETTING = "com.android.settings.PRIMARY_PROFILE_CONTROLLED"; + private static final String SETTINGS_PACKAGE = "com.android.settings"; + private static final String WEBVIEW_IMPLEMENTATION_ACTIVITY = ".WebViewImplementation"; @Override public void onReceive(Context context, Intent broadcast) { @@ -100,7 +102,7 @@ public class SettingsInitialize extends BroadcastReceiver { return; } ComponentName settingsComponentName = - new ComponentName(context, WebViewImplementation.class); + new ComponentName(SETTINGS_PACKAGE, SETTINGS_PACKAGE + WEBVIEW_IMPLEMENTATION_ACTIVITY); pm.setComponentEnabledSetting(settingsComponentName, userInfo.isAdmin() ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : diff --git a/src/com/android/settings/WebViewImplementation.java b/src/com/android/settings/WebViewImplementation.java deleted file mode 100644 index 349f5e93b83..00000000000 --- a/src/com/android/settings/WebViewImplementation.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the - * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.android.settings; - -import android.annotation.Nullable; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.content.DialogInterface.OnDismissListener; -import android.os.Bundle; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.UserManager; -import android.util.Log; -import android.webkit.IWebViewUpdateService; -import android.webkit.WebViewProviderInfo; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.core.InstrumentedActivity; - -import java.util.ArrayList; - -public class WebViewImplementation extends InstrumentedActivity implements - OnCancelListener, OnDismissListener { - - private static final String TAG = "WebViewImplementation"; - - private IWebViewUpdateService mWebViewUpdateService; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (!UserManager.get(this).isAdminUser()) { - finish(); - return; - } - mWebViewUpdateService = - IWebViewUpdateService.Stub.asInterface(ServiceManager.getService("webviewupdate")); - try { - WebViewProviderInfo[] providers = mWebViewUpdateService.getValidWebViewPackages(); - if (providers == null) { - Log.e(TAG, "No WebView providers available"); - finish(); - return; - } - - String currentValue = mWebViewUpdateService.getCurrentWebViewPackageName(); - if (currentValue == null) { - currentValue = ""; - } - - int currentIndex = -1; - ArrayList options = new ArrayList<>(); - final ArrayList values = new ArrayList<>(); - for (WebViewProviderInfo provider : providers) { - if (Utils.isPackageEnabled(this, provider.packageName)) { - options.add(provider.description); - values.add(provider.packageName); - if (currentValue.contentEquals(provider.packageName)) { - currentIndex = values.size() - 1; - } - } - } - - new AlertDialog.Builder(this) - .setTitle(R.string.select_webview_provider_dialog_title) - .setSingleChoiceItems(options.toArray(new String[0]), currentIndex, - new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - try { - mWebViewUpdateService.changeProviderAndSetting(values.get(which)); - } catch (RemoteException e) { - Log.w(TAG, "Problem reaching webviewupdate service", e); - } - finish(); - } - }).setNegativeButton(android.R.string.cancel, null) - .setOnCancelListener(this) - .setOnDismissListener(this) - .show(); - } catch (RemoteException e) { - Log.w(TAG, "Problem reaching webviewupdate service", e); - finish(); - } - } - - @Override - public int getMetricsCategory() { - return MetricsEvent.WEBVIEW_IMPLEMENTATION; - } - - @Override - public void onCancel(DialogInterface dialog) { - finish(); - } - - @Override - public void onDismiss(DialogInterface dialog) { - finish(); - } -} diff --git a/src/com/android/settings/webview/UserPackageWrapper.java b/src/com/android/settings/webview/UserPackageWrapper.java new file mode 100644 index 00000000000..8fbb10cb8a1 --- /dev/null +++ b/src/com/android/settings/webview/UserPackageWrapper.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.settings.webview; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.UserInfo; +import android.webkit.UserPackage; + +import java.util.List; + +/** + * Wrapper class around android.webkit.UserPackage - to be able to use UserPackage in Robolectric + * tests (such tests currently don't support mocking hidden classes). + */ +interface UserPackageWrapper { + UserInfo getUserInfo(); + PackageInfo getPackageInfo(); + boolean isEnabledPackage(); + boolean isInstalledPackage(); +} diff --git a/src/com/android/settings/webview/UserPackageWrapperImpl.java b/src/com/android/settings/webview/UserPackageWrapperImpl.java new file mode 100644 index 00000000000..1ea7c2e7927 --- /dev/null +++ b/src/com/android/settings/webview/UserPackageWrapperImpl.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.settings.webview; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.UserInfo; +import android.webkit.UserPackage; + +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation of UserPackageWrapper. + */ +class UserPackageWrapperImpl implements UserPackageWrapper { + private final UserPackage mUserPackage; + + UserPackageWrapperImpl(UserPackage userPackage) { + mUserPackage = userPackage; + } + + public UserInfo getUserInfo() { + return mUserPackage.getUserInfo(); + } + + public PackageInfo getPackageInfo() { + return mUserPackage.getPackageInfo(); + } + + public boolean isEnabledPackage() { + return mUserPackage.isEnabledPackage(); + } + + public boolean isInstalledPackage() { + return mUserPackage.isInstalledPackage(); + } +} diff --git a/src/com/android/settings/webview/WebViewAppListAdapter.java b/src/com/android/settings/webview/WebViewAppListAdapter.java new file mode 100644 index 00000000000..85dbf7c302a --- /dev/null +++ b/src/com/android/settings/webview/WebViewAppListAdapter.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.settings.webview; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.support.annotation.VisibleForTesting; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; + +import com.android.settings.applications.AppViewHolder; +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * Custom list adapter for Settings to choose WebView package. + * Note: parts of this class are copied from AppPicker.java. + */ +class WebViewAppListAdapter extends ArrayAdapter { + private final LayoutInflater mInflater; + + public WebViewAppListAdapter(Context context, + WebViewUpdateServiceWrapper webviewUpdateServiceWrapper) { + super(context, 0); + mInflater = LayoutInflater.from(context); + + final List packageInfoList = + new ArrayList(); + List pkgs = + webviewUpdateServiceWrapper.getValidWebViewApplicationInfos(getContext()); + for (ApplicationInfo ai : pkgs) { + WebViewApplicationInfo info = new WebViewApplicationInfo(ai, + ai.loadLabel(context.getPackageManager()).toString(), + getDisabledReason(webviewUpdateServiceWrapper, context, ai.packageName)); + packageInfoList.add(info); + } + addAll(packageInfoList); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // A ViewHolder keeps references to children views to avoid unnecessary calls + // to findViewById() on each row. + AppViewHolder holder = AppViewHolder.createOrRecycle(mInflater, convertView); + convertView = holder.rootView; + WebViewApplicationInfo info = getItem(position); + holder.appName.setText(info.label); + if (info.info != null) { + holder.appIcon.setImageDrawable(info.info.loadIcon(getContext().getPackageManager())); + // Allow disable-description to wrap - to be able to show several lines of text in case + // a package is disabled/uninstalled for several users. + holder.summary.setSingleLine(false); + if (!isEnabled(position)) { + holder.summary.setText(info.disabledReason); + } else { + holder.summary.setText(""); + } + } else { + holder.appIcon.setImageDrawable(null); + holder.summary.setText(""); + } + holder.disabled.setVisibility(View.GONE); + // Only allow a package to be chosen if it is enabled and installed for all users. + convertView.setEnabled(isEnabled(position)); + return convertView; + } + + @Override + public boolean isEnabled (int position) { + WebViewApplicationInfo info = getItem(position); + return info.disabledReason == null; + } + + @Override + public boolean areAllItemsEnabled() { + int numItems = getCount(); + for (int n = 0; n < numItems; n++) { + if (!isEnabled(n)) return false; + } + return true; + } + + /** + * Returns the reason why a package cannot be used as WebView implementation. + * This is either because of it being disabled, uninstalled, or hidden for any user. + */ + @VisibleForTesting + static String getDisabledReason(WebViewUpdateServiceWrapper webviewUpdateServiceWrapper, + Context context, String packageName) { + StringBuilder disabledReason = new StringBuilder(); + List userPackages = + webviewUpdateServiceWrapper.getPackageInfosAllUsers(context, packageName); + for (UserPackageWrapper userPackage : userPackages) { + if (!userPackage.isInstalledPackage()) { + // Package uninstalled/hidden + disabledReason.append(context.getString( + R.string.webview_uninstalled_for_user, userPackage.getUserInfo().name)); + } else if (!userPackage.isEnabledPackage()) { + // Package disabled + disabledReason.append(context.getString( + R.string.webview_disabled_for_user, userPackage.getUserInfo().name)); + } + } + if (disabledReason.length() == 0) return null; + return disabledReason.toString(); + } +} + diff --git a/src/com/android/settings/webview/WebViewAppPicker.java b/src/com/android/settings/webview/WebViewAppPicker.java new file mode 100644 index 00000000000..2417b004bd6 --- /dev/null +++ b/src/com/android/settings/webview/WebViewAppPicker.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.webview; + +import android.app.ListActivity; +import android.content.pm.PackageInfo; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.support.annotation.VisibleForTesting; +import android.util.Log; +import android.view.View; +import android.webkit.WebViewFactory; +import android.widget.ListView; + +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.settings.core.instrumentation.Instrumentable; +import com.android.settings.core.instrumentation.VisibilityLoggerMixin; + +public class WebViewAppPicker extends ListActivity implements Instrumentable { + private static final String TAG = WebViewAppPicker.class.getSimpleName(); + private WebViewAppListAdapter mAdapter; + private WebViewUpdateServiceWrapper mWebViewUpdateServiceWrapper; + + private final VisibilityLoggerMixin mVisibilityLoggerMixin = + new VisibilityLoggerMixin(getMetricsCategory()); + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + if (mWebViewUpdateServiceWrapper == null) { + setWebViewUpdateServiceWrapper(createDefaultWebViewUpdateServiceWrapper()); + } + mAdapter = new WebViewAppListAdapter(this, mWebViewUpdateServiceWrapper); + setListAdapter(mAdapter); + + mVisibilityLoggerMixin.onAttach(this); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + WebViewApplicationInfo app = mAdapter.getItem(position); + + if (mWebViewUpdateServiceWrapper.setWebViewProvider(app.info.packageName)) { + Intent intent = new Intent(); + intent.setAction(app.info.packageName); + setResult(RESULT_OK, intent); + } else { + mWebViewUpdateServiceWrapper.showInvalidChoiceToast(this); + } + finish(); + } + + private WebViewUpdateServiceWrapper createDefaultWebViewUpdateServiceWrapper() { + return new WebViewUpdateServiceWrapper(); + } + + @VisibleForTesting + void setWebViewUpdateServiceWrapper(WebViewUpdateServiceWrapper wvusWrapper) { + mWebViewUpdateServiceWrapper = wvusWrapper; + } + + @Override + public void onResume() { + super.onResume(); + mVisibilityLoggerMixin.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + mVisibilityLoggerMixin.onPause(); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.WEBVIEW_IMPLEMENTATION; + } +} diff --git a/src/com/android/settings/webview/WebViewAppPreferenceController.java b/src/com/android/settings/webview/WebViewAppPreferenceController.java new file mode 100644 index 00000000000..eb5467a3428 --- /dev/null +++ b/src/com/android/settings/webview/WebViewAppPreferenceController.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.settings.webview; + +import android.app.Activity; +import android.content.pm.PackageInfo; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.VisibleForTesting; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settings.DevelopmentSettings; +import com.android.settings.core.PreferenceController; + +public class WebViewAppPreferenceController extends PreferenceController { + + private static final String WEBVIEW_APP_KEY = "select_webview_provider"; + + private Context mContext; + private Preference mPreference; + private final WebViewUpdateServiceWrapper mWebViewUpdateServiceWrapper; + + public WebViewAppPreferenceController(Context context) { + this(context, new WebViewUpdateServiceWrapper()); + } + + public WebViewAppPreferenceController(Context context, + WebViewUpdateServiceWrapper webviewUpdateServiceWrapper) { + super(context); + mContext = context; + mWebViewUpdateServiceWrapper = webviewUpdateServiceWrapper; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (getPreferenceKey().equals(preference.getKey())) { + return true; + } + return false; + } + + public Intent getActivityIntent() { + return new Intent(mContext, WebViewAppPicker.class); + } + + @Override + public void updateState(Preference preference) { + mPreference.setSummary(getCurrentWebViewPackageLabel(mContext)); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + if (isAvailable()) { + mPreference = screen.findPreference(WEBVIEW_APP_KEY); + } + } + + /** + * Handle the return-value from the WebViewAppPicker Activity. + */ + public void onActivityResult(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + updateState(null); + } + } + + private String getCurrentWebViewPackageLabel(Context context) { + PackageInfo webViewPackage = mWebViewUpdateServiceWrapper.getCurrentWebViewPackage(); + if (webViewPackage == null) return ""; + return webViewPackage.applicationInfo.loadLabel(context.getPackageManager()).toString(); + } + + + @Override + public String getPreferenceKey() { + return WEBVIEW_APP_KEY; + } + + @Override + public boolean isAvailable() { + return true; + } + + public void enablePreference(boolean enabled) { + if (isAvailable()) { + mPreference.setEnabled(enabled); + } + } +} diff --git a/src/com/android/settings/webview/WebViewApplicationInfo.java b/src/com/android/settings/webview/WebViewApplicationInfo.java new file mode 100644 index 00000000000..6879c59f48a --- /dev/null +++ b/src/com/android/settings/webview/WebViewApplicationInfo.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.settings.webview; + +import android.content.pm.ApplicationInfo; + +final class WebViewApplicationInfo { + final ApplicationInfo info; + final String label; + final String disabledReason; + + public WebViewApplicationInfo(ApplicationInfo info, String label, String disabledReason) { + this.info = info; + this.label = label; + this.disabledReason = disabledReason; + } +} diff --git a/src/com/android/settings/webview/WebViewUpdateServiceWrapper.java b/src/com/android/settings/webview/WebViewUpdateServiceWrapper.java new file mode 100644 index 00000000000..b40be1941fa --- /dev/null +++ b/src/com/android/settings/webview/WebViewUpdateServiceWrapper.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.webview; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.util.Log; +import android.webkit.UserPackage; +import android.webkit.WebViewFactory; +import android.webkit.WebViewProviderInfo; +import android.widget.Toast; + +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.List; + +class WebViewUpdateServiceWrapper { + private static final String TAG = "WVUSWrapper"; + + public WebViewUpdateServiceWrapper() {} + + /** + * Fetch the package currently used as WebView implementation. + */ + public PackageInfo getCurrentWebViewPackage() { + try { + return WebViewFactory.getUpdateService().getCurrentWebViewPackage(); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + } + return null; + } + + /** + * Fetches ApplicationInfo objects for all currently valid WebView packages. + * A WebView package is considered valid if it can be used as a WebView implementation. The + * validity of a package is not dependent on whether the package is installed/enabled. + */ + public List getValidWebViewApplicationInfos(Context context) { + WebViewProviderInfo[] providers = null; + try { + providers = WebViewFactory.getUpdateService().getValidWebViewPackages(); + } catch (RemoteException e) { + } + List pkgs = new ArrayList<>(); + for (WebViewProviderInfo provider : providers) { + try { + pkgs.add(context.getPackageManager().getApplicationInfo( + provider.packageName, PACKAGE_FLAGS)); + } catch (PackageManager.NameNotFoundException e) { + } + } + return pkgs; + } + + /** + * Change WebView provider to {@param packageName}. + * @return whether the change succeeded. + */ + public boolean setWebViewProvider(String packageName) { + try { + return packageName.equals( + WebViewFactory.getUpdateService().changeProviderAndSetting(packageName)); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException when trying to change provider to " + packageName, e); + } + return false; + } + + /** + * Fetch PackageInfos for the package named {@param packageName} for all users on the device. + */ + public List getPackageInfosAllUsers(Context context, String packageName) { + List userPackageWrappers = new ArrayList<>(); + List userPackages = + UserPackage.getPackageInfosAllUsers(context, packageName, PACKAGE_FLAGS); + for (UserPackage userPackage : userPackages) { + userPackageWrappers.add(new UserPackageWrapperImpl(userPackage)); + } + return userPackageWrappers; + } + + /** + * Show a toast to explain the chosen package can no longer be chosen. + */ + public void showInvalidChoiceToast(Context context) { + // The user chose a package that became invalid since the list was last updated, + // show a Toast to explain the situation. + Toast toast = Toast.makeText(context, + R.string.select_webview_provider_toast_text, Toast.LENGTH_SHORT); + toast.show(); + } + + static final int PACKAGE_FLAGS = PackageManager.MATCH_ANY_USER; +} diff --git a/tests/robotests/src/com/android/settings/webview/WebViewAppListAdapterTest.java b/tests/robotests/src/com/android/settings/webview/WebViewAppListAdapterTest.java new file mode 100644 index 00000000000..a8ab5d46f56 --- /dev/null +++ b/tests/robotests/src/com/android/settings/webview/WebViewAppListAdapterTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.webview; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.UserInfo; + +import com.android.settings.R; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class WebViewAppListAdapterTest { + private Context mContext = RuntimeEnvironment.application; + + private final static UserInfo FIRST_USER = new UserInfo(0, "FIRST_USER", 0); + private final static UserInfo SECOND_USER = new UserInfo(0, "SECOND_USER", 0); + + private final static String DEFAULT_PACKAGE_NAME = "DEFAULT_PACKAGE_NAME"; + + @Test + public void testDisabledReasonNullIfPackagesOk() { + UserPackageWrapper packageForFirstUser = mock(UserPackageWrapper.class); + when(packageForFirstUser.isEnabledPackage()).thenReturn(true); + when(packageForFirstUser.isInstalledPackage()).thenReturn(true); + + UserPackageWrapper packageForSecondUser = mock(UserPackageWrapper.class); + when(packageForSecondUser.isEnabledPackage()).thenReturn(true); + when(packageForSecondUser.isInstalledPackage()).thenReturn(true); + + WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class); + when(wvusWrapper.getPackageInfosAllUsers( + any(), eq(DEFAULT_PACKAGE_NAME))).thenReturn( + Arrays.asList(packageForFirstUser, packageForSecondUser)); + + assertThat(WebViewAppListAdapter.getDisabledReason( + wvusWrapper, mContext, DEFAULT_PACKAGE_NAME)).isNull(); + } + + @Test + public void testDisabledReasonForSingleUserDisabledPackage() { + UserPackageWrapper packageForFirstUser = mock(UserPackageWrapper.class); + when(packageForFirstUser.isEnabledPackage()).thenReturn(false); + when(packageForFirstUser.isInstalledPackage()).thenReturn(true); + when(packageForFirstUser.getUserInfo()).thenReturn(FIRST_USER); + + WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class); + when(wvusWrapper.getPackageInfosAllUsers(any(), eq(DEFAULT_PACKAGE_NAME) + )).thenReturn(Arrays.asList(packageForFirstUser)); + + assertThat(WebViewAppListAdapter.getDisabledReason(wvusWrapper, mContext, + DEFAULT_PACKAGE_NAME)).isEqualTo("Disabled for user " + FIRST_USER.name + "\n"); + } + + @Test + public void testDisabledReasonForSingleUserUninstalledPackage() { + UserPackageWrapper packageForFirstUser = mock(UserPackageWrapper.class); + when(packageForFirstUser.isEnabledPackage()).thenReturn(true); + when(packageForFirstUser.isInstalledPackage()).thenReturn(false); + when(packageForFirstUser.getUserInfo()).thenReturn(FIRST_USER); + + WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class); + when(wvusWrapper.getPackageInfosAllUsers(any(), eq(DEFAULT_PACKAGE_NAME) + )).thenReturn(Arrays.asList(packageForFirstUser)); + + assertThat(WebViewAppListAdapter.getDisabledReason(wvusWrapper, mContext, + DEFAULT_PACKAGE_NAME)).isEqualTo("Uninstalled for user " + FIRST_USER.name + "\n"); + } + + @Test + public void testDisabledReasonSeveralUsers() { + UserPackageWrapper packageForFirstUser = mock(UserPackageWrapper.class); + when(packageForFirstUser.isEnabledPackage()).thenReturn(false); + when(packageForFirstUser.isInstalledPackage()).thenReturn(true); + when(packageForFirstUser.getUserInfo()).thenReturn(FIRST_USER); + + UserPackageWrapper packageForSecondUser = mock(UserPackageWrapper.class); + when(packageForSecondUser.isEnabledPackage()).thenReturn(true); + when(packageForSecondUser.isInstalledPackage()).thenReturn(false); + when(packageForSecondUser.getUserInfo()).thenReturn(SECOND_USER); + + WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class); + when(wvusWrapper.getPackageInfosAllUsers(any(), eq(DEFAULT_PACKAGE_NAME) + )).thenReturn(Arrays.asList(packageForFirstUser, packageForSecondUser)); + + final String EXPECTED_DISABLED_REASON = String.format( + "Disabled for user %s\nUninstalled for user %s\n", + FIRST_USER.name, SECOND_USER.name); + assertThat(WebViewAppListAdapter.getDisabledReason( + wvusWrapper, mContext,DEFAULT_PACKAGE_NAME)).isEqualTo(EXPECTED_DISABLED_REASON); + } + + /** + * Ensure we only proclaim a package as uninstalled for a certain user if it's both uninstalled + * and disabled. + */ + @Test + public void testDisabledReasonUninstalledAndDisabled() { + UserPackageWrapper packageForFirstUser = mock(UserPackageWrapper.class); + when(packageForFirstUser.isEnabledPackage()).thenReturn(false); + when(packageForFirstUser.isInstalledPackage()).thenReturn(false); + when(packageForFirstUser.getUserInfo()).thenReturn(FIRST_USER); + + UserPackageWrapper packageForSecondUser = mock(UserPackageWrapper.class); + when(packageForSecondUser.isEnabledPackage()).thenReturn(true); + when(packageForSecondUser.isInstalledPackage()).thenReturn(true); + when(packageForSecondUser.getUserInfo()).thenReturn(SECOND_USER); + + WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class); + when(wvusWrapper.getPackageInfosAllUsers(any(), eq(DEFAULT_PACKAGE_NAME) + )).thenReturn(Arrays.asList(packageForFirstUser, packageForSecondUser)); + + final String EXPECTED_DISABLED_REASON = String.format( + "Uninstalled for user %s\n", FIRST_USER.name); + assertThat(WebViewAppListAdapter.getDisabledReason(wvusWrapper, mContext, + DEFAULT_PACKAGE_NAME)).isEqualTo(EXPECTED_DISABLED_REASON); + } +} diff --git a/tests/robotests/src/com/android/settings/webview/WebViewAppPickerTest.java b/tests/robotests/src/com/android/settings/webview/WebViewAppPickerTest.java new file mode 100644 index 00000000000..8ace8aadd5a --- /dev/null +++ b/tests/robotests/src/com/android/settings/webview/WebViewAppPickerTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.webview; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.shadows.ShadowView.clickOn; +import static org.robolectric.Shadows.shadowOf; + +import android.app.Activity; +import android.content.pm.ApplicationInfo; +import android.view.View; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; +import org.robolectric.util.ActivityController; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class WebViewAppPickerTest { + + private static final String DEFAULT_PACKAGE_NAME = "DEFAULT_PACKAGE_NAME"; + + private static ApplicationInfo createApplicationInfo(String packageName) { + ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + return ai; + } + + @Test + public void testClickingItemChangesProvider() { + ActivityController controller = + Robolectric.buildActivity(WebViewAppPicker.class); + WebViewAppPicker webviewAppPicker = controller.get(); + + WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class); + when(wvusWrapper.getValidWebViewApplicationInfos(any())).thenReturn( + Arrays.asList(createApplicationInfo(DEFAULT_PACKAGE_NAME))); + when(wvusWrapper.setWebViewProvider(eq(DEFAULT_PACKAGE_NAME))).thenReturn(true); + + webviewAppPicker.setWebViewUpdateServiceWrapper(wvusWrapper); + + controller.create().start().postCreate(null).resume().visible(); + WebViewApplicationInfo firstItem = + (WebViewApplicationInfo) webviewAppPicker.getListView().getItemAtPosition(0); + assertThat(firstItem.info.packageName).isEqualTo(DEFAULT_PACKAGE_NAME); + + webviewAppPicker.onListItemClick(webviewAppPicker.getListView(), null, 0, 0); + + verify(wvusWrapper, times(1)).setWebViewProvider(eq(DEFAULT_PACKAGE_NAME)); + assertThat(shadowOf(webviewAppPicker).getResultCode()).isEqualTo(Activity.RESULT_OK); + verify(wvusWrapper, never()).showInvalidChoiceToast(any()); + } + + @Test + public void testFailingPackageChangeReturnsCancelled() { + ActivityController controller = + Robolectric.buildActivity(WebViewAppPicker.class); + WebViewAppPicker webviewAppPicker = controller.get(); + + WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class); + when(wvusWrapper.getValidWebViewApplicationInfos(any())).thenReturn( + Arrays.asList(createApplicationInfo(DEFAULT_PACKAGE_NAME))); + when(wvusWrapper.setWebViewProvider(eq(DEFAULT_PACKAGE_NAME))).thenReturn(false); + + webviewAppPicker.setWebViewUpdateServiceWrapper(wvusWrapper); + + controller.create().start().postCreate(null).resume().visible(); + WebViewApplicationInfo firstItem = + (WebViewApplicationInfo) webviewAppPicker.getListView().getItemAtPosition(0); + assertThat(firstItem.info.packageName).isEqualTo(DEFAULT_PACKAGE_NAME); + + webviewAppPicker.onListItemClick(webviewAppPicker.getListView(), null, 0, 0); + + verify(wvusWrapper, times(1)).setWebViewProvider(eq(DEFAULT_PACKAGE_NAME)); + assertThat(shadowOf(webviewAppPicker).getResultCode()).isEqualTo(Activity.RESULT_CANCELED); + verify(wvusWrapper, times(1)).showInvalidChoiceToast(any()); + } +} diff --git a/tests/robotests/src/com/android/settings/webview/WebViewAppPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/webview/WebViewAppPreferenceControllerTest.java new file mode 100644 index 00000000000..c16bd1f472f --- /dev/null +++ b/tests/robotests/src/com/android/settings/webview/WebViewAppPreferenceControllerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.settings.webview; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class WebViewAppPreferenceControllerTest { + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Context mContext; + + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private Preference mPreference; + + private static final String DEFAULT_PACKAGE_NAME = "DEFAULT_PACKAGE_NAME"; + + @Before public void setUp() { + MockitoAnnotations.initMocks(this); + when(mPreferenceScreen.findPreference(any())).thenReturn(mPreference); + } + + @Test public void testOnActivityResultUpdatesStateOnSuccess() { + WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class); + WebViewAppPreferenceController controller = + spy(new WebViewAppPreferenceController(mContext, wvusWrapper)); + + controller.displayPreference(mPreferenceScreen); // Makes sure Preference is non-null + controller.onActivityResult(Activity.RESULT_OK, new Intent(DEFAULT_PACKAGE_NAME)); + verify(controller, times(1)).updateState(any()); + } + + @Test public void testOnActivityResultWithFailureDoesNothing() { + WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class); + + WebViewAppPreferenceController controller = + spy(new WebViewAppPreferenceController(mContext, wvusWrapper)); + + controller.displayPreference(mPreferenceScreen); // Makes sure Preference is non-null + controller.onActivityResult(Activity.RESULT_CANCELED, new Intent(DEFAULT_PACKAGE_NAME)); + verify(controller, never()).updateState(any()); + } +}