From 4d3334c50b07c60c137085a35a8fb670eabd2aca Mon Sep 17 00:00:00 2001 From: Gustav Sennton Date: Tue, 13 Dec 2016 19:16:48 +0000 Subject: [PATCH] Update WebView implementation Dev Setting - new looks + descriptions. Now that we are changing the WebView package mechanism so that a package can only be used as WebView implementation if that package is enabled for all users of the device we need to tell the user why a package can't be chosen as WebView implementation. With this CL we do so in the 'Set WebView Implementation' Developer Setting. We code for the WebView implementation Developer Setting is now based on the same code as that of the Debug App Developer Setting. Bug: 32894154 Test: Ensure WebView Implementation setting shows correct packages. Test: Ensure WebView Implementation setting shows correct descriptions for why a package is not usable - including the case where packages are disabled/uninstalled for a second user. Test: Ensure the summary for the WebView Implmentation setting is updated after changing WebView package. Test: Ensure the WebView package Activity (Settings.ACTION_WEBVIEW_SETTINGS) is similar to the Dev Setting. Test: ensure non-admin user cannot start WV-picker activity through 'adb shell am start -n com.android.settings/.WebViewImplementation' Change-Id: Ia6e6e9e12ce8f8f45ec539807cd0c6479acb8ecb --- AndroidManifest.xml | 9 +- res/values/strings.xml | 5 + res/xml/development_prefs.xml | 6 +- .../android/settings/DevelopmentSettings.java | 82 ++-------- .../android/settings/SettingsInitialize.java | 4 +- .../settings/WebViewImplementation.java | 114 -------------- .../settings/webview/UserPackageWrapper.java | 33 ++++ .../webview/UserPackageWrapperImpl.java | 50 ++++++ .../webview/WebViewAppListAdapter.java | 126 +++++++++++++++ .../settings/webview/WebViewAppPicker.java | 95 +++++++++++ .../WebViewAppPreferenceController.java | 103 ++++++++++++ .../webview/WebViewApplicationInfo.java | 29 ++++ .../webview/WebViewUpdateServiceWrapper.java | 113 +++++++++++++ .../webview/WebViewAppListAdapterTest.java | 149 ++++++++++++++++++ .../webview/WebViewAppPickerTest.java | 109 +++++++++++++ .../WebViewAppPreferenceControllerTest.java | 83 ++++++++++ 16 files changed, 922 insertions(+), 188 deletions(-) delete mode 100644 src/com/android/settings/WebViewImplementation.java create mode 100644 src/com/android/settings/webview/UserPackageWrapper.java create mode 100644 src/com/android/settings/webview/UserPackageWrapperImpl.java create mode 100644 src/com/android/settings/webview/WebViewAppListAdapter.java create mode 100644 src/com/android/settings/webview/WebViewAppPicker.java create mode 100644 src/com/android/settings/webview/WebViewAppPreferenceController.java create mode 100644 src/com/android/settings/webview/WebViewApplicationInfo.java create mode 100644 src/com/android/settings/webview/WebViewUpdateServiceWrapper.java create mode 100644 tests/robotests/src/com/android/settings/webview/WebViewAppListAdapterTest.java create mode 100644 tests/robotests/src/com/android/settings/webview/WebViewAppPickerTest.java create mode 100644 tests/robotests/src/com/android/settings/webview/WebViewAppPreferenceControllerTest.java 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()); + } +}