diff --git a/app/build.gradle b/app/build.gradle index ad69a2418..1ba09d9b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -230,19 +230,32 @@ configurations { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') + + // Common implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'com.android.support:design:27.1.1' + implementation 'com.google.vr:sdk-audio:1.170.0' + + // Android Components + implementation "com.github.mozilla:mozillaspeechlibrary:1.0.6" + implementation "org.mozilla.components:service-telemetry:${rootProject.ext.androidComponents['version']}" + implementation "org.mozilla.components:browser-errorpages:${rootProject.ext.androidComponents['version']}" + implementation "org.mozilla.components:browser-search:${rootProject.ext.androidComponents['version']}" + implementation "org.mozilla.components:browser-domains:${rootProject.ext.androidComponents['version']}" + implementation "org.mozilla.components:ui-autocomplete:${rootProject.ext.androidComponents['version']}" + + // Testing testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + + // Daydream googlevrImplementation 'com.google.vr:sdk-base:1.170.0' googlevrFlatImplementation 'com.google.vr:sdk-base:1.170.0' + + // ODG svrImplementation fileTree(dir: "${project.rootDir}/third_party/svr/", include: ['*.jar']) - implementation 'com.android.support:design:27.1.1' - implementation 'com.google.vr:sdk-audio:1.170.0' - implementation "org.mozilla.components:service-telemetry:${rootProject.ext.androidComponents['version']}" - implementation "org.mozilla.components:browser-errorpages:${rootProject.ext.androidComponents['version']}" - implementation "com.github.mozilla:mozillaspeechlibrary:1.0.6" } if (findProject(':servo')) { @@ -266,7 +279,6 @@ if (findProject(':geckoview-local')) { dependencies { // To see what the latest geckoview-nightly version is go here: // https://maven.mozilla.org/?prefix=maven2/org/mozilla/geckoview/geckoview-nightly-armeabi-v7a/ - armImplementation "org.mozilla.geckoview:geckoview-nightly-armeabi-v7a:${rootProject.ext.geckoNightly['version']}" x86Implementation "org.mozilla.geckoview:geckoview-nightly-x86:${rootProject.ext.geckoNightly['version']}" } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java index 996f0c015..a69afb722 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java @@ -36,8 +36,9 @@ import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.crashreporting.CrashReporterService; import org.mozilla.vrbrowser.crashreporting.GlobalExceptionHandler; +import org.mozilla.vrbrowser.geolocation.GeolocationWrapper; import org.mozilla.vrbrowser.input.MotionEventGenerator; -import org.mozilla.vrbrowser.search.SearchEngine; +import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.OffscreenDisplay; import org.mozilla.vrbrowser.ui.widgets.BrowserWidget; @@ -116,6 +117,7 @@ public void run() { private Thread mUiThread; private LinkedList> mBrightnessQueue; private Pair mCurrentBrightness; + private SearchEngineWrapper mSearchEngineWrapper; @Override protected void onCreate(Bundle savedInstanceState) { @@ -129,6 +131,7 @@ protected void onCreate(Bundle savedInstanceState) { Bundle extras = getIntent() != null ? getIntent().getExtras() : null; SessionStore.get().setContext(this, extras); + SessionStore.get().registerListeners(); // Create broadcast receiver for getting crash messages from crash process IntentFilter intentFilter = new IntentFilter(); @@ -170,7 +173,11 @@ protected void onCreate(Bundle savedInstanceState) { queueRunnable(() -> setTemporaryFilePath(tempPath)); initializeWorld(); - SearchEngine.get(this).update(); + // Setup the search engine + mSearchEngineWrapper = SearchEngineWrapper.get(this); + mSearchEngineWrapper.registerForUpdates(); + + GeolocationWrapper.update(this); } protected void initializeWorld() { @@ -244,6 +251,7 @@ protected void onResume() { protected void onDestroy() { // Unregister the crash service broadcast receiver unregisterReceiver(mCrashReceiver); + mSearchEngineWrapper.unregisterForUpdates(); for (Widget widget: mWidgets.values()) { widget.releaseWidget(); @@ -259,7 +267,7 @@ protected void onDestroy() { mPermissionDelegate.release(); } - SessionStore.get().clearListeners(); + SessionStore.get().unregisterListeners(); super.onDestroy(); } @@ -483,7 +491,7 @@ void handleAudioPose(float qx, float qy, float qz, float qw, float px, float py, mAudioEngine.setPose(qx, qy, qz, qw, px, py, pz); // https://developers.google.com/vr/reference/android/com/google/vr/sdk/audio/GvrAudioEngine.html#resume() - // The update method must be called from the main thread at a regular rate. + // The initialize method must be called from the main thread at a regular rate. runOnUiThread(mAudioUpdateRunnable); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java index 769e070ef..70f24fe17 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/SessionStore.java @@ -6,8 +6,10 @@ package org.mozilla.vrbrowser.browser; import android.content.Context; +import android.content.SharedPreferences; import android.graphics.Rect; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -27,9 +29,9 @@ import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.crashreporting.CrashReporterService; +import org.mozilla.vrbrowser.geolocation.GeolocationData; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.utils.InternalPages; -import org.mozilla.vrbrowser.utils.ValueHolder; import java.io.File; import java.io.FileNotFoundException; @@ -47,11 +49,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import static org.mozilla.vrbrowser.utils.ServoUtils.*; +import static org.mozilla.vrbrowser.utils.ServoUtils.createServoSession; +import static org.mozilla.vrbrowser.utils.ServoUtils.isInstanceOfServoSession; +import static org.mozilla.vrbrowser.utils.ServoUtils.isServoAvailable; public class SessionStore implements GeckoSession.NavigationDelegate, GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, GeckoSession.TextInputDelegate, GeckoSession.TrackingProtectionDelegate, - GeckoSession.PromptDelegate { + GeckoSession.PromptDelegate, SharedPreferences.OnSharedPreferenceChangeListener { private static SessionStore mInstance; private static final String LOGTAG = "VRB"; @@ -110,8 +114,15 @@ class State { private int mPreviousSessionId = SessionStore.NO_SESSION_ID; private String mRegion; private Context mContext; + private SharedPreferences mPrefs; private SessionStore() { + mSessions = new LinkedHashMap<>(); + mSessionsStack = new ArrayDeque<>(); + mPrivateSessionsStack = new ArrayDeque<>(); + } + + public void registerListeners() { mNavigationListeners = new LinkedList<>(); mProgressListeners = new LinkedList<>(); mContentListeners = new LinkedList<>(); @@ -119,17 +130,21 @@ private SessionStore() { mTextInputListeners = new LinkedList<>(); mPromptListeners = new LinkedList<>(); - mSessions = new LinkedHashMap<>(); - mSessionsStack = new ArrayDeque<>(); - mPrivateSessionsStack = new ArrayDeque<>(); + if (mPrefs != null) { + mPrefs.registerOnSharedPreferenceChangeListener(this); + } } - public void clearListeners() { + public void unregisterListeners() { mNavigationListeners.clear(); mProgressListeners.clear(); mContentListeners.clear(); mSessionChangeListeners.clear(); mTextInputListeners.clear(); + + if (mPrefs != null) { + mPrefs.unregisterOnSharedPreferenceChangeListener(this); + } } public void setContext(Context aContext, Bundle aExtras) { @@ -157,6 +172,7 @@ public void setContext(Context aContext, Bundle aExtras) { } mContext = aContext; + mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); } public void dumpAllState(Integer sessionId) { @@ -441,7 +457,7 @@ public void setRegion(String aRegion) { Log.d(LOGTAG, "SessionStore setRegion: " + aRegion); mRegion = aRegion != null ? aRegion.toLowerCase() : "worldwide"; - // There is a region update and the home is already loaded + // There is a region initialize and the home is already loaded if (mCurrentSession != null && isHomeUri(getCurrentUri())) { mCurrentSession.loadUri("javascript:window.location.replace('" + getHomeUri() + "');"); } @@ -1173,4 +1189,16 @@ public void onFilePrompt(GeckoSession session, String title, int type, String[] public GeckoResult onPopupRequest(final GeckoSession session, final String targetUri) { return GeckoResult.fromValue(AllowOrDeny.DENY); } + + // SharedPreferences.OnSharedPreferenceChangeListener + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (mContext != null) { + if (key == mContext.getString(R.string.settings_key_geolocation_data)) { + GeolocationData data = GeolocationData.parse(sharedPreferences.getString(key, null)); + setRegion(data.getCountryCode()); + } + } + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/geolocation/GeolocationClient.java b/app/src/common/shared/org/mozilla/vrbrowser/geolocation/GeolocationClient.java new file mode 100644 index 000000000..aaffd42cb --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/geolocation/GeolocationClient.java @@ -0,0 +1,35 @@ +package org.mozilla.vrbrowser.geolocation; + +import com.loopj.android.http.AsyncHttpClient; +import com.loopj.android.http.JsonHttpResponseHandler; + +import org.json.JSONObject; + +import java.util.function.Function; + +import cz.msebera.android.httpclient.Header; + +public class GeolocationClient { + + private static final int RETRY_SLEEP = 5 * 1000; + + private static AsyncHttpClient client = new AsyncHttpClient(); + + public static void getGeolocation(String aQuery, int retries, Function success, Function error) { + client.cancelAllRequests(true); + client.setMaxRetriesAndTimeout(retries, RETRY_SLEEP); + client.get(aQuery, null, new JsonHttpResponseHandler("ISO-8859-1") { + + @Override + public void onSuccess(int statusCode, Header[] headers, JSONObject response) { + success.apply(GeolocationData.parse(response.toString())); + } + + @Override + public void onFailure(int statusCode, Header[] headers, Throwable throwable, JSONObject errorResponse) { + error.apply(errorResponse); + } + + }); + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/geolocation/GeolocationData.java b/app/src/common/shared/org/mozilla/vrbrowser/geolocation/GeolocationData.java new file mode 100644 index 000000000..9fc1b11af --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/geolocation/GeolocationData.java @@ -0,0 +1,51 @@ +package org.mozilla.vrbrowser.geolocation; + +import android.support.annotation.NonNull; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Class representing a Geolocation success response (HTTP 200) + */ +public class GeolocationData { + + private static final String LOGTAG = "VRB"; + + private JSONObject mData; + + private GeolocationData(JSONObject data) { + mData = data; + } + + @NonNull + public static GeolocationData create(JSONObject data) { + return new GeolocationData(data); + } + + @NonNull + public static GeolocationData parse(String aGeolocationJson) { + try { + return GeolocationData.create(new JSONObject(aGeolocationJson)); + + } catch (JSONException e) { + Log.e(LOGTAG, "Error parsing geolocation data: " + e.getLocalizedMessage()); + return null; + } + } + + public String getCountryCode() { + return mData.optString("country_code", ""); + } + + public String getCountryName() { + return mData.optString("country_name", ""); + } + + @Override + public String toString() { + return mData.toString(); + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/geolocation/GeolocationWrapper.java b/app/src/common/shared/org/mozilla/vrbrowser/geolocation/GeolocationWrapper.java new file mode 100644 index 000000000..ba5da5487 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/geolocation/GeolocationWrapper.java @@ -0,0 +1,52 @@ +package org.mozilla.vrbrowser.geolocation; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.mozilla.gecko.util.ThreadUtils; +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.browser.SettingsStore; + +public class GeolocationWrapper { + + private static final int MAX_RETRIES = 2; + private static final int RETRY_SLEEP = 5 * 1000; + + public static void update(final @NonNull Context aContext) { + String endpoint = aContext.getString(R.string.geolocation_api_url); + update(aContext, endpoint, 0, MAX_RETRIES); + } + + private static void update(final @NonNull Context aContext, + final @NonNull String endPoint, + final int retryCount, + final int maxRetries) { + if (retryCount <= maxRetries - 1) { + GeolocationClient.getGeolocation( + endPoint, + MAX_RETRIES, + (data) -> { + if (data == null) { + if (retryCount <= maxRetries) { + ThreadUtils.postDelayedToUiThread(() -> + update(aContext, endPoint, retryCount + 1, maxRetries), + RETRY_SLEEP); + } + + } else { + SettingsStore.getInstance(aContext).setGeolocationData(data.toString()); + } + return null; + }, + (error) -> { + if (retryCount <= maxRetries) { + ThreadUtils.postDelayedToUiThread(() -> + update(aContext, endPoint, retryCount + 1, maxRetries), + RETRY_SLEEP); + } + return null; + }); + } + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java b/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java new file mode 100644 index 000000000..5041b58af --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationLocalizationProvider.java @@ -0,0 +1,41 @@ +package org.mozilla.vrbrowser.search; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.mozilla.vrbrowser.geolocation.GeolocationData; + +import java.util.Locale; + +import mozilla.components.browser.search.provider.localization.SearchLocalizationProvider; + +public class GeolocationLocalizationProvider extends SearchLocalizationProvider { + + private String mCountry; + private String mLanguage; + private String mRegion; + + GeolocationLocalizationProvider(GeolocationData data) { + mCountry = data.getCountryCode(); + mLanguage = Locale.getDefault().getLanguage(); + mRegion = data.getCountryCode(); + } + + @NotNull + @Override + public String getCountry() { + return mCountry; + } + + @NotNull + @Override + public String getLanguage() { + return mLanguage; + } + + @Nullable + @Override + public String getRegion() { + return mRegion; + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationTask.java b/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationTask.java deleted file mode 100644 index 4b1754634..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/search/GeolocationTask.java +++ /dev/null @@ -1,269 +0,0 @@ -package org.mozilla.vrbrowser.search; - -import android.os.AsyncTask; -import android.support.annotation.NonNull; -import android.util.Log; -import org.json.JSONException; -import org.json.JSONObject; - -import javax.net.ssl.HttpsURLConnection; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URL; - -public class GeolocationTask extends AsyncTask { - - private static final String LOGTAG = "VRB"; - - private static final int MAX_RETRIES = 2; - private static final int CONNECTION_TIMEOUT = 10 * 1000; - - public static class GeolocationError { - private int mCode; - private String mMessage; - - private GeolocationError(int aCode, String aMessage) { - mCode = aCode; - mMessage = aMessage; - } - - @NonNull - public static GeolocationError create(int aCode, String aMessage) { - return new GeolocationError(aCode, aMessage); - } - - public static GeolocationError parse(String aGeolocationJson) { - GeolocationError data = null; - - JSONObject json; - try { - json = new JSONObject(aGeolocationJson); - JSONObject error = json.getJSONObject("error"); - - return GeolocationError.create( - error.getInt("code"), - error.getString("message")); - - } catch (JSONException e) { - Log.e(LOGTAG, "Error parsing geolocation data: " + e.getLocalizedMessage()); - } - - return data; - } - - public int getCode() { - return mCode; - } - - public String getMessage() { - return mMessage; - } - } - - public static class GeolocationData { - - private String mCountryCode; - private String mCountryName; - - private GeolocationData(String aCountryCode, String aCountryName) { - mCountryCode = aCountryCode; - mCountryName = aCountryName; - } - - @NonNull - public static GeolocationData create(String aCountryCode, String aCountryName) { - return new GeolocationData(aCountryCode, aCountryName); - } - - public static GeolocationData parse(String aGeolocationJson) { - GeolocationData data = new GeolocationData(null, null); - - JSONObject json; - try { - json = new JSONObject(aGeolocationJson); - data.mCountryCode = json.getString("country_code"); - data.mCountryName = json.getString("country_name"); - - } catch (JSONException e) { - Log.e(LOGTAG, "Error parsing geolocation data: " + e.getLocalizedMessage()); - - data.mCountryCode = ""; - data.mCountryName = ""; - } - - return data; - } - - public String getCountryCode() { - return mCountryCode; - } - - public String getCountryName() { - return mCountryName; - } - - public String toString() { - JSONObject jsonObject = new JSONObject(); - try { - jsonObject.put("country_code", mCountryCode); - jsonObject.put("country_name", mCountryName); - - } catch (JSONException e) { - Log.e(LOGTAG, "Error: " + e.getLocalizedMessage()); - } - - return jsonObject.toString(); - } - } - - public enum ResponseType { - SUCCESS, - ERROR - } - - public static class GeolocationTaskResponse { - - ResponseType responseType; - Object data; - - public static GeolocationTaskResponse create(ResponseType type, Object data) { - GeolocationTaskResponse response = new GeolocationTaskResponse(); - response.responseType = type; - response.data = data; - - return response; - } - - } - - public interface GeolocationTaskDelegate { - void onGeolocationRequestStarted(); - void onGeolocationRequestSuccess(GeolocationData response); - void onGeolocationRequestError(String error); - } - - private GeolocationTaskDelegate mDelegate; - private int mRetries; - private int mRetryCount; - private String mEndpoint; - - public GeolocationTask(@NonNull String endpoint, GeolocationTaskDelegate aDelegate) { - this(endpoint, aDelegate, MAX_RETRIES); - } - - public GeolocationTask(@NonNull String endpoint, GeolocationTaskDelegate aDelegate, int retries) { - mEndpoint = endpoint; - mDelegate = aDelegate; - mRetries = retries; - mRetryCount = 0; - } - - @Override - protected GeolocationTaskResponse doInBackground(Void... params) { - if (mDelegate != null) - mDelegate.onGeolocationRequestStarted(); - - GeolocationTaskResponse result; - do { - result = executeGeoLocationRequest(); - - if (result.responseType == ResponseType.ERROR) { - if (mRetryCount <= mRetries - 1) { - Log.e(LOGTAG, "Geolocation request error, retrying... " + (mRetryCount + 1)); - - } else { - Log.e(LOGTAG, "Max geolocation request retry count reached. Cancelling"); - result = GeolocationTaskResponse.create(ResponseType.ERROR, "Max retry count reached"); - } - } - } while(mRetryCount++ < mRetries && result.responseType == ResponseType.ERROR); - - return result; - } - - @NonNull - private GeolocationTaskResponse executeGeoLocationRequest() { - HttpsURLConnection urlConnection = null; - BufferedReader reader = null; - - try { - URL url = new URL(mEndpoint); - - urlConnection = (HttpsURLConnection) url.openConnection(); - urlConnection.setRequestMethod("GET"); - urlConnection.setConnectTimeout(CONNECTION_TIMEOUT); - urlConnection.connect(); - - InputStream inputStream = urlConnection.getInputStream(); - StringBuffer buffer = new StringBuffer(); - if (inputStream == null) { - Log.e(LOGTAG, "Null input stream"); - return GeolocationTaskResponse.create(ResponseType.ERROR, "Null input stream"); - } - - reader = new BufferedReader(new InputStreamReader(inputStream)); - - String line; - while ((line = reader.readLine()) != null) { - buffer.append(line + "\n"); - } - - if (buffer.length() == 0) { - Log.e(LOGTAG, "Empty response buffer"); - return GeolocationTaskResponse.create(ResponseType.ERROR, "Empty response buffer"); - } - - String result = buffer.toString(); - - if (urlConnection.getResponseCode() != 200) { - GeolocationError error = GeolocationError.parse(result); - if (error != null) - return GeolocationTaskResponse.create(ResponseType.SUCCESS, error); - else - return GeolocationTaskResponse.create(ResponseType.ERROR, "Server error: " + urlConnection.getResponseCode()); - } - - GeolocationData data = GeolocationData.parse(result); - return GeolocationTaskResponse.create(ResponseType.SUCCESS, data); - - } catch (IOException e) { - Log.e(LOGTAG, "Error: " + e.getLocalizedMessage()); - - return GeolocationTaskResponse.create(ResponseType.ERROR, "Error: " + e.getLocalizedMessage()); - - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - if (reader != null) { - try { - reader.close(); - } catch (final IOException e) { - Log.e(LOGTAG, "Error closing stream: " + e.getLocalizedMessage()); - } - } - } - } - - @Override - protected void onPostExecute(GeolocationTaskResponse response) { - super.onPostExecute(response); - - if (mDelegate != null) { - if (response.responseType == ResponseType.SUCCESS) { - if (response.data instanceof GeolocationData) { - mDelegate.onGeolocationRequestSuccess((GeolocationData) response.data); - - } else if (response.data instanceof GeolocationError) { - GeolocationError error = (GeolocationError)response.data; - mDelegate.onGeolocationRequestError(error.getMessage()); - } - - } else { - mDelegate.onGeolocationRequestError((String)response.data); - } - } - } -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngine.java b/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngine.java deleted file mode 100644 index b047d5226..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngine.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.mozilla.vrbrowser.search; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.util.Log; -import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.browser.SessionStore; -import org.mozilla.vrbrowser.browser.SettingsStore; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; - -public class SearchEngine implements GeolocationTask.GeolocationTaskDelegate { - - private static final String LOGTAG = "VRB"; - - private static SearchEngine mSearchEngineInstance; - - private static class Engine { - - private int mUrlResource; - private int mQueryResource; - - public String getURLResource(Context aContext) { - return aContext.getString(mUrlResource); - } - - public String getSearchQuery(Context aContext, String aQuery) { - return aContext.getString(mUrlResource) + "?" + aContext.getString(mQueryResource, aQuery); - } - - public static Engine create(int urlResource, int queryResource) { - Engine engine = new Engine(); - engine.mUrlResource = urlResource; - engine.mQueryResource = queryResource; - - return engine; - } - - @NonNull - public static Engine getEngine(@NonNull GeolocationTask.GeolocationData data) { - String countryCode = data.getCountryCode().toUpperCase(); - if (countryCode.equals("US")) - return Engine.create(R.string.search_google_us, R.string.search_google_us_params); - - else if (countryCode.equals("CN")) - return Engine.create(R.string.search_baidu_cn, R.string.search_baidu_params); - - else if (countryCode.equals("RU")) - return Engine.create(R.string.search_yandex_ru, R.string.search_yandex_params); - - else if (countryCode.equals("BY")) - return Engine.create(R.string.search_yandex_by, R.string.search_yandex_params); - - else if (countryCode.equals("TR")) - return Engine.create(R.string.search_yandex_tr, R.string.search_yandex_params); - - else if (countryCode.equals("KZ")) - return Engine.create(R.string.search_yandex_kz, R.string.search_yandex_params); - - else - return Engine.create(R.string.search_google, R.string.search_google_params); - } - } - - public static synchronized @NonNull - SearchEngine get(final @NonNull Context aContext) { - if (mSearchEngineInstance == null) { - mSearchEngineInstance = new SearchEngine(aContext); - } - - return mSearchEngineInstance; - } - - private Context mContext; - private Engine mEngine; - private boolean isUpdating; - private GeolocationTask mTask; - private String mEndpoint; - - private SearchEngine(@NonNull Context aContext) { - mContext = aContext; - isUpdating = false; - mEndpoint = mContext.getString(R.string.geolocation_api_url); - - String geolocationJson = SettingsStore.getInstance(mContext).getGeolocationData(); - GeolocationTask.GeolocationData data = GeolocationTask.GeolocationData.parse(geolocationJson); - mEngine = Engine.getEngine(data); - SessionStore.get().setRegion(data.getCountryCode()); - } - - public String getSearchURL(String aQuery) { - try { - aQuery = URLEncoder.encode(aQuery, "UTF-8"); - - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - return mEngine.getSearchQuery(mContext, aQuery); - } - public String getURLResource() { - return mEngine.getURLResource(mContext); - } - - public void update() { - if (!isUpdating) { - mTask = new GeolocationTask(mEndpoint, this); - mTask.execute(); - - } else { - Log.i(LOGTAG, "Geolocation update cancelled, previous update already running"); - } - } - - @Override - public void onGeolocationRequestStarted() { - isUpdating = true; - } - - @Override - public void onGeolocationRequestSuccess(GeolocationTask.GeolocationData data) { - isUpdating = false; - - SettingsStore.getInstance(mContext).setGeolocationData(data.toString()); - - mEngine = Engine.getEngine(data); - SessionStore.get().setRegion(data.getCountryCode()); - - Log.d(LOGTAG, "Geolocation request success: " + data.toString()); - } - - @Override - public void onGeolocationRequestError(String error) { - isUpdating = false; - - Log.e(LOGTAG, "Geolocation request error: " + error); - } -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngineWrapper.java b/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngineWrapper.java new file mode 100644 index 000000000..9902a1e56 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/search/SearchEngineWrapper.java @@ -0,0 +1,207 @@ +package org.mozilla.vrbrowser.search; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; + +import org.jetbrains.annotations.Contract; +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.geolocation.GeolocationData; +import org.mozilla.vrbrowser.search.suggestions.SuggestionsClient; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import kotlin.coroutines.experimental.Continuation; +import mozilla.components.browser.search.SearchEngine; +import mozilla.components.browser.search.SearchEngineManager; +import mozilla.components.browser.search.provider.AssetsSearchEngineProvider; +import mozilla.components.browser.search.provider.filter.SearchEngineFilter; +import mozilla.components.browser.search.provider.localization.LocaleSearchLocalizationProvider; +import mozilla.components.browser.search.provider.localization.SearchLocalizationProvider; +import mozilla.components.browser.search.suggestions.SearchSuggestionClient; + +public class SearchEngineWrapper implements SharedPreferences.OnSharedPreferenceChangeListener { + + // Specific FxR engine overrides. US is already overridden by browser-search component + // https://github.com/MozillaReality/FirefoxReality/issues/248#issuecomment-412278211 + private static final Map REGION_ENGINE_OVERRIDE = new HashMap() {{ + put("CN", "baidu"); + put("RU", "yandex-ru"); + put("BY", "yandex.by"); + put("TR", "yandex-tr"); + put("KZ", "yandex-kz"); + }}; + + private static String EMPTY = ""; + + private static SearchEngineWrapper mSearchEngineWrapperInstance; + + public static synchronized @NonNull + SearchEngineWrapper get(final @NonNull Context aContext) { + if (mSearchEngineWrapperInstance == null) { + mSearchEngineWrapperInstance = new SearchEngineWrapper(aContext); + } + + return mSearchEngineWrapperInstance; + } + + public interface SuggestionsDelegate { + void OnSuggestions(List aSuggestionsList); + } + + private Context mContext; + private SearchEngine mSearchEngine; + private SearchLocalizationProvider mLocalizationProvider; + private SearchEngineManager mSearchEngineManager; + private SearchSuggestionClient mSuggestionsClient; + private SharedPreferences mPrefs; + + private SearchEngineWrapper(@NonNull Context aContext) { + mContext = aContext; + mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext); + + setupSearchEngine(aContext, EMPTY); + } + + public void registerForUpdates() { + if (mContext != null) { + mContext.registerReceiver( + mLocaleChangedReceiver, + new IntentFilter(Intent.ACTION_LOCALE_CHANGED)); + if (mPrefs != null) { + mPrefs.registerOnSharedPreferenceChangeListener(this); + } + } + } + + public void unregisterForUpdates() { + if (mContext != null) { + mContext.unregisterReceiver(mLocaleChangedReceiver); + if (mPrefs != null) { + mPrefs.unregisterOnSharedPreferenceChangeListener(this); + } + } + } + + public String getSearchURL(String aQuery) { + try { + aQuery = URLEncoder.encode(aQuery, "UTF-8"); + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return mSearchEngine.buildSearchUrl(aQuery); + } + + public String getSuggestionURL(String aQuery) { + try { + aQuery = URLEncoder.encode(aQuery, "UTF-8"); + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return mSearchEngine.buildSuggestionsURL(aQuery); + } + + public void getSuggestions(String aQuery, SuggestionsDelegate delegate) { + // TODO: Use mSuggestionsClient.getSuggestions when fixed in browser-search. + String query = getSuggestionURL(aQuery); + SuggestionsClient.getSuggestions(mSearchEngine, query, result -> { + delegate.OnSuggestions(result); + return null; + }); + } + + public String getResourceURL() { + Uri uri = Uri.parse(mSearchEngine.buildSearchUrl("")) ; + return uri.getScheme() + "://" + uri.getHost(); + } + + // Receiver for locale updates + private BroadcastReceiver mLocaleChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() == Intent.ACTION_LOCALE_CHANGED) { + setupSearchEngine(context, EMPTY); + } + } + }; + + /** + * We cannot send system ACTION_LOCALE_CHANGED so the component refreshes the engines + * with the updated SearchLocalizationProvider information so we have to update the whole manager. + * @param aContext Activity context + * @param userPref User preferred engine (among the available ones) + */ + private void setupSearchEngine(@NonNull Context aContext, String userPref) { + List engineFilterList = new ArrayList<>(); + + GeolocationData data = GeolocationData.parse(SettingsStore.getInstance(aContext).getGeolocationData()); + if (data == null) { + // If we don't have geolocation data we default to the Locale search localization provider + mLocalizationProvider = new LocaleSearchLocalizationProvider(); + + } else { + // If we have geolocation data we initialize the provider with the received data + // and setup a filter to filter the engines that we need to override for FxR. + mLocalizationProvider = new GeolocationLocalizationProvider(data); + if (getEngine(data.getCountryCode()) != null) { + SearchEngineFilter engineFilter = (ctx, searchEngine) -> + searchEngine.getIdentifier().equalsIgnoreCase(getEngine(data.getCountryCode())); + engineFilterList.add(engineFilter); + } + } + + // Configure the assets search with the localization provider and the engines that we want + // to filter. + AssetsSearchEngineProvider engineProvider = new AssetsSearchEngineProvider( + mLocalizationProvider, + engineFilterList, + Collections.emptyList()); + + mSearchEngineManager = new SearchEngineManager(Arrays.asList(engineProvider)); + + // If we don't get any result we use the default configuration. + if (mSearchEngineManager.getSearchEngines(aContext).size() == 0) { + mSearchEngineManager = new SearchEngineManager(); + } + + // A name can be used if the user get's to choose among the available engines + mSearchEngine = mSearchEngineManager.getDefaultSearchEngine(aContext, userPref); + mSuggestionsClient = new SearchSuggestionClient(mSearchEngine, this::domainAutocompleteFilter); + } + + @NonNull + @Contract(pure = true) + private Object domainAutocompleteFilter(String aQuery, Continuation aContinuation) { + return "[\"firefox\",[\"firefox\",\"firefox for mac\",\"firefox quantum\",\"firefox update\",\"firefox esr\",\"firefox focus\",\"firefox addons\",\"firefox extensions\",\"firefox nightly\",\"firefox clear cache\"]]"; + } + + private String getEngine(String aCountryCode) { + return REGION_ENGINE_OVERRIDE.get(aCountryCode); + } + + // SharedPreferences.OnSharedPreferenceChangeListener + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (mContext != null) { + if (key == mContext.getString(R.string.settings_key_geolocation_data)) { + setupSearchEngine(mContext, EMPTY); + } + } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionParser.java b/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionParser.java new file mode 100644 index 000000000..031bc371a --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/search/suggestions/SuggestionParser.java @@ -0,0 +1,102 @@ +package org.mozilla.vrbrowser.search.suggestions; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import mozilla.components.browser.search.SearchEngine; + +public class SuggestionParser { + + private static final String AZERDICT = "Azerdict"; + private static final String DAUM = "다음지도"; + private static final String QWANT = "Qwant"; + + public static Function> selectResponseParser(SearchEngine mEngine) { + if (mEngine.getName().equals(AZERDICT)) { + return azerdictResponseParser; + + } else if (mEngine.getName().equals(DAUM)) { + return daumResponseParser; + + } else if (mEngine.getName().equals(QWANT)) { + return qwantResponseParser; + } + + return defaultResponseParser; + } + + private static Function> defaultResponseParser = buildJSONArrayParser(1); + private static Function> azerdictResponseParser = buildJSONObjectParser("suggestions"); + private static Function> daumResponseParser = buildJSONObjectParser("items"); + private static Function> qwantResponseParser = buildQwantParser(); + + private static Function> buildJSONArrayParser(int ressultsIndex) { + return input -> { + List list = new ArrayList<>(); + try { + JSONArray root = new JSONArray(input); + JSONArray array = root.getJSONArray(ressultsIndex); + if (array != null) { + int len = array.length(); + for (int i=0; i> buildJSONObjectParser(String resultsKey) { + return input -> { + List list = new ArrayList<>(); + try { + JSONObject root = new JSONObject(input); + JSONArray array = root.getJSONArray(resultsKey); + if (array != null) { + int len = array.length(); + for (int i=0; i> buildQwantParser() { + return input -> { + List list = new ArrayList<>(); + try { + JSONObject root = new JSONObject(input); + JSONObject data = root.getJSONObject("data"); + JSONArray items = data.getJSONArray("items"); + if (items != null) { + int len = items.length(); + for (int i=0; i, Void> callback) { + client.cancelAllRequests(true); + client.get(aQuery, null, new TextHttpResponseHandler("ISO-8859-1") { + @Override + public void onFailure(int statusCode, Header[] headers, String responseString, Throwable throwable) { + } + + @Override + public void onSuccess(int statusCode, Header[] headers, String responseString) { + callback.apply(SuggestionParser.selectResponseParser(mEngine).apply(responseString)); + } + }); + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java index 363c460cb..52b8cac7a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/TelemetryWrapper.java @@ -21,7 +21,7 @@ import org.mozilla.vrbrowser.BuildConfig; import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.browser.SettingsStore; -import org.mozilla.vrbrowser.search.SearchEngine; +import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.utils.UrlUtils; import java.net.URI; @@ -178,7 +178,7 @@ public static void voiceInputEvent() { } private static String getDefaultSearchEngineIdentifierForTelemetry(Context aContext) { - return SearchEngine.get(aContext).getURLResource(); + return SearchEngineWrapper.get(aContext).getResourceURL(); } private static void searchEnterEvent() { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java index afa6ec8d6..c7847dd4d 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/NavigationURLBar.java @@ -14,17 +14,21 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.view.GestureDetector; -import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; -import android.widget.*; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; + import org.mozilla.vrbrowser.R; import org.mozilla.vrbrowser.browser.SessionStore; -import org.mozilla.vrbrowser.search.SearchEngine; +import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; +import org.mozilla.vrbrowser.utils.UrlUtils; import java.io.UnsupportedEncodingException; import java.net.URI; @@ -32,9 +36,13 @@ import java.net.URLDecoder; import java.util.regex.Pattern; +import kotlin.Unit; +import mozilla.components.browser.domains.DomainAutoCompleteProvider; +import mozilla.components.ui.autocomplete.InlineAutocompleteEditText; + public class NavigationURLBar extends FrameLayout { - private EditText mURL; + private InlineAutocompleteEditText mURL; private ImageButton mMicrophoneButton; private ImageView mInsecureIcon; private ImageView mLoadingView; @@ -45,11 +53,25 @@ public class NavigationURLBar extends FrameLayout { private int mDefaultURLLeftPadding = 0; private int mURLProtocolColor; private int mURLWebsiteColor; - private Pattern mURLPattern; private NavigationURLBarDelegate mDelegate; + private DomainAutoCompleteProvider mAutocompleteProvider; + + private Unit domainAutocompleteFilter(String text, InlineAutocompleteEditText view) { + if (view != null) { + DomainAutoCompleteProvider.Result result = mAutocompleteProvider.autocomplete(text); + view.applyAutocompleteResult(new InlineAutocompleteEditText.AutocompleteResult( + result.getText(), + result.getSource(), + result.getSize(), + null)); + } + return Unit.INSTANCE; + } public interface NavigationURLBarDelegate { void OnVoiceSearchClicked(); + void OnShowSearchPopup(); + void OnHideSearchPopup(); } public NavigationURLBar(Context context, AttributeSet attrs) { @@ -59,40 +81,41 @@ public NavigationURLBar(Context context, AttributeSet attrs) { private void initialize(Context aContext) { inflate(aContext, R.layout.navigation_url, this); - mURLPattern = Pattern.compile("[\\d\\w][.][\\d\\w]"); + + // Use Domain autocomplete provider from components + mAutocompleteProvider = new DomainAutoCompleteProvider(); + mAutocompleteProvider.initialize(aContext, true, true, true); + mURL = findViewById(R.id.urlEditText); mURL.setShowSoftInputOnFocus(false); - mURL.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView aTextView, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_DONE) { - handleURLEdit(aTextView.getText().toString()); - return true; - } - return false; - } + mURL.setOnEditorActionListener((aTextView, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + handleURLEdit(aTextView.getText().toString()); + return true; + } + return false; }); - mURL.setOnFocusChangeListener(new OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - showVoiceSearch(!b || (mURL.getText().length() == 0)); - mURL.setSelection(mURL.getText().length(), 0); - } + mURL.setOnFocusChangeListener((view, focused) -> { + showVoiceSearch(!focused || (mURL.getText().length() == 0)); + + mURL.setSelection(mURL.getText().length(), 0); }); + final GestureDetector gd = new GestureDetector(getContext(), new UrlGestureListener()); gd.setOnDoubleTapListener(mUrlDoubleTapListener); - mURL.setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent motionEvent) { - view.requestFocusFromTouch(); - if (gd.onTouchEvent(motionEvent)) { - return true; - } - return view.onTouchEvent(motionEvent); + mURL.setOnTouchListener((view, motionEvent) -> { + view.requestFocusFromTouch(); + if (gd.onTouchEvent(motionEvent)) { + return true; } + return view.onTouchEvent(motionEvent); }); mURL.addTextChangedListener(mURLTextWatcher); + + // Set a filter to provide domain autocomplete results + mURL.setOnFilterListener(this::domainAutocompleteFilter); + mURL.setFocusable(true); mURL.setFocusableInTouchMode(true); @@ -166,6 +189,14 @@ else if (aURL.startsWith("javascript:") && mURL.addTextChangedListener(mURLTextWatcher); } + public String getText() { + return mURL.getText().toString(); + } + + public String getOriginalText() { + return mURL.getOriginalText(); + } + public void setIsInsecure(boolean aIsInsecure) { if (mIsInsecure != aIsInsecure) { mIsInsecure = aIsInsecure; @@ -221,7 +252,7 @@ public void handleURLEdit(String text) { String urlText = text; // Detect when the protocol is missing from the URL. // Look for a separated '.' in the text with no white spaces. - if (!hasProtocol && !urlText.contains(" ") && mURLPattern.matcher(urlText).find()) { + if (!hasProtocol && !urlText.contains(" ") && UrlUtils.isDomain(urlText)) { urlText = "https://" + urlText; hasProtocol = true; } @@ -240,7 +271,7 @@ public void handleURLEdit(String text) { } else if (text.startsWith("about:") || text.startsWith("resource://")) { url = text; } else { - url = SearchEngine.get(getContext()).getSearchURL(text); + url = SearchEngineWrapper.get(getContext()).getSearchURL(text); // Doing search in the URL bar, so sending "aIsURL: false" to telemetry. TelemetryWrapper.urlBarEvent(false); @@ -248,6 +279,10 @@ public void handleURLEdit(String text) { if (SessionStore.get().getCurrentUri() != url) { SessionStore.get().loadUri(url); + + if (mDelegate != null) { + mDelegate.OnHideSearchPopup(); + } } showVoiceSearch(text.isEmpty()); @@ -303,7 +338,9 @@ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { @Override public void afterTextChanged(Editable editable) { - + if (mDelegate != null) { + mDelegate.OnShowSearchPopup(); + } } }; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java index a60a762c6..6488fce78 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java @@ -26,11 +26,13 @@ import org.mozilla.vrbrowser.audio.AudioEngine; import org.mozilla.vrbrowser.browser.SessionStore; import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.search.SearchEngineWrapper; import org.mozilla.vrbrowser.utils.AnimationHelper; import org.mozilla.vrbrowser.ui.views.CustomUIButton; import org.mozilla.vrbrowser.ui.views.NavigationURLBar; import org.mozilla.vrbrowser.ui.views.UIButton; import org.mozilla.vrbrowser.ui.views.UITextButton; +import org.mozilla.vrbrowser.utils.UrlUtils; import java.util.ArrayList; import java.util.Arrays; @@ -39,7 +41,7 @@ public class NavigationBarWidget extends UIWidget implements GeckoSession.Naviga GeckoSession.ProgressDelegate, GeckoSession.ContentDelegate, WidgetManagerDelegate.UpdateListener, SessionStore.SessionChangeListener, NavigationURLBar.NavigationURLBarDelegate, VoiceSearchWidget.VoiceSearchDelegate, - SharedPreferences.OnSharedPreferenceChangeListener { + SharedPreferences.OnSharedPreferenceChangeListener, SuggestionsWidget.URLBarPopupDelegate { private static final String LOGTAG = "VRB"; @@ -69,6 +71,8 @@ public class NavigationBarWidget extends UIWidget implements GeckoSession.Naviga private VoiceSearchWidget mVoiceSearchWidget; private Context mAppContext; private SharedPreferences mPrefs; + private SuggestionsWidget mPopup; + private SearchEngineWrapper mSearchEngineWrapper; public NavigationBarWidget(Context aContext) { super(aContext); @@ -257,6 +261,11 @@ public void onClick(View view) { mVoiceSearchWidget = createChild(VoiceSearchWidget.class, false); mVoiceSearchWidget.setDelegate(this); + mPopup = createChild(SuggestionsWidget.class); + mPopup.setURLBarPopupDelegate(this); + + mSearchEngineWrapper = SearchEngineWrapper.get(getContext()); + SessionStore.get().addSessionChangeListener(this); mPrefs = PreferenceManager.getDefaultSharedPreferences(mAppContext); @@ -608,6 +617,8 @@ public void onCurrentSessionChange(GeckoSession aSession, int aId) { } } + // NavigationURLBarDelegate + @Override public void OnVoiceSearchClicked() { if (mVoiceSearchWidget.getPlacement().visible) { @@ -618,6 +629,80 @@ public void OnVoiceSearchClicked() { } } + @Override + public void OnShowSearchPopup() { + if (mPopup != null) { + final String text = mURLBar.getText().trim(); + final String originalText = mURLBar.getOriginalText().trim(); + if (originalText.length() > 0) { + mSearchEngineWrapper.getSuggestions( + originalText, + (suggestions) -> { + ArrayList items = new ArrayList<>(); + + if (!text.equals(originalText)) { + // Completion from browser-domains + items.add(SuggestionsWidget.SuggestionItem.create( + text, + getSearchURLOrDomain(text), + null, + SuggestionsWidget.SuggestionItem.Type.COMPLETION + )); + } + + // Original text + items.add(SuggestionsWidget.SuggestionItem.create( + originalText, + getSearchURLOrDomain(originalText), + null, + SuggestionsWidget.SuggestionItem.Type.SUGGESTION + )); + + // Suggestions + for (String suggestion : suggestions) { + String url = mSearchEngineWrapper.getSearchURL(suggestion); + items.add(SuggestionsWidget.SuggestionItem.create( + suggestion, + url, + null, + SuggestionsWidget.SuggestionItem.Type.SUGGESTION + )); + } + mPopup.setItems(items); + mPopup.setHighlightedText(originalText); + + if (!mPopup.isVisible()) { + mPopup.getPlacement().width = (int) (WidgetPlacement.convertPixelsToDp(getContext(), mURLBar.getWidth())); + mPopup.updatePlacement(); + mPopup.show(); + } + } + ); + + } else { + mPopup.hide(); + } + } + } + + @Override + public void OnHideSearchPopup() { + if (mPopup != null && mPopup.isVisible()) { + mPopup.hide(); + } + } + + private String getSearchURLOrDomain(String text) { + if (UrlUtils.isDomain(text)) { + return text; + + } else { + return mSearchEngineWrapper.getSearchURL(text); + } + } + + // VoiceSearch Delegate + @Override public void OnVoiceSearchResult(String transcription, float confidance) { mURLBar.handleURLEdit(transcription); @@ -639,4 +724,16 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin updateServoButton(); } } + + // URLBarPopupWidgetDelegate + + @Override + public void OnItemClicked(SuggestionsWidget.SuggestionItem item) { + mURLBar.handleURLEdit(item.url); + } + + @Override + public void OnItemDeleted(SuggestionsWidget.SuggestionItem item) { + + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/SuggestionsWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/SuggestionsWidget.java new file mode 100644 index 000000000..9b6da339a --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/SuggestionsWidget.java @@ -0,0 +1,377 @@ +package org.mozilla.vrbrowser.ui.widgets; + +import android.content.Context; +import android.graphics.Typeface; +import android.support.annotation.NonNull; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.StyleSpan; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ArrayAdapter; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.audio.AudioEngine; + +import java.util.ArrayList; +import java.util.List; + +public class SuggestionsWidget extends UIWidget implements WidgetManagerDelegate.FocusChangeListener { + + private ListView mList; + private SuggestionsAdapter mAdapter; + private List mListItems; + private Animation mScaleUpAnimation; + private Animation mScaleDownAnimation; + private URLBarPopupDelegate mURLBarDelegate; + private String mHighlightedText; + private AudioEngine mAudio; + + public interface URLBarPopupDelegate { + void OnItemClicked(SuggestionItem item); + void OnItemDeleted(SuggestionItem item); + } + + public SuggestionsWidget(Context aContext) { + super(aContext); + initialize(aContext); + } + + public SuggestionsWidget(Context aContext, AttributeSet aAttrs) { + super(aContext, aAttrs); + initialize(aContext); + } + + public SuggestionsWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { + super(aContext, aAttrs, aDefStyle); + initialize(aContext); + } + + private void initialize(Context aContext) { + inflate(aContext, R.layout.list_popup_window, this); + + mWidgetManager.addFocusChangeListener(this); + + mList = findViewById(R.id.list); + mList.setSoundEffectsEnabled(false); + + mScaleUpAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.popup_scaleup); + mScaleDownAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.popup_scaledown); + mScaleDownAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + + } + + @Override + public void onAnimationEnd(Animation animation) { + SuggestionsWidget.super.hide(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }); + + mListItems = new ArrayList<>(); + + mAudio = AudioEngine.fromContext(aContext); + + mHighlightedText = ""; + } + + @Override + public void releaseWidget() { + mWidgetManager.removeFocusChangeListener(this); + + super.releaseWidget(); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + aPlacement.visible = false; + aPlacement.worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.url_bar_popup_world_width); + aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.url_bar_popup_height); + aPlacement.parentAnchorX = 0.0f; + aPlacement.parentAnchorY = 1.0f; + aPlacement.anchorX = 0.0f; + aPlacement.anchorY = 0.0f; + aPlacement.translationX = WidgetPlacement.unitFromMeters(getContext(), R.dimen.url_bar_popup_world_x); + aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.url_bar_popup_world_z); + aPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.url_bar_popup_world_y); + } + + @Override + public void show() { + super.show(false); + mList.startAnimation(mScaleUpAnimation); + } + + @Override + public void hide() { + mList.startAnimation(mScaleDownAnimation); + } + + @Override + public void handleResizeEvent(float aWorldWidth, float aWorldHeight) { + mWidgetPlacement.worldWidth = aWorldWidth * (mWidgetPlacement.width/getWorldWidth()); + mWidgetManager.updateWidget(this); + } + + // FocusChangeListener + + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (oldFocus != null && isVisible()) { + onDismiss(); + } + } + + public void setURLBarPopupDelegate(URLBarPopupDelegate aDelegate) { + mURLBarDelegate = aDelegate; + } + + public void setHighlightedText(String text) { + mHighlightedText = text; + } + + public void setItems(List items) { + mListItems = items; + mAdapter = new SuggestionsAdapter(getContext(), R.layout.list_popup_window_item, mListItems); + mList.setAdapter(mAdapter); + } + + public void updatePlacement() { + float worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.browser_world_width); + float aspect = mWidgetPlacement.width / mWidgetPlacement.height; + float worldHeight = worldWidth / aspect; + float area = worldWidth * worldHeight; + + float targetWidth = (float) Math.sqrt(area * aspect); + float targetHeight = (float) Math.sqrt(area / aspect); + + handleResizeEvent(targetWidth, targetHeight); + } + + public static class SuggestionItem { + + public enum Type { + BOOKMARK, + FAVORITE, + HISTORY, + SUGGESTION, + COMPLETION + } + + public String faviconURL; + public String text; + public String url; + public Type type = Type.SUGGESTION; + + public static SuggestionItem create(@NonNull String text, String url, String faviconURL, Type type) { + SuggestionItem item = new SuggestionItem(); + item.text = text; + item.url = url; + item.faviconURL = faviconURL; + item.type = type; + + return item; + } + } + + public class SuggestionsAdapter extends ArrayAdapter { + + private class ItemViewHolder { + ViewGroup layout; + ImageView favicon; + TextView title; + TextView url; + ImageButton delete; + View divider; + } + + private LayoutInflater mInflater; + + public SuggestionsAdapter(@NonNull Context context, int resource, @NonNull List objects) { + super(context, resource, objects); + + mInflater = LayoutInflater.from(getContext()); + } + + public View getView(int position, View convertView, ViewGroup parent) { + View listItem = convertView; + + ItemViewHolder itemViewHolder; + if(listItem == null) { + listItem = mInflater.inflate(R.layout.list_popup_window_item, parent, false); + + itemViewHolder = new ItemViewHolder(); + + itemViewHolder.layout = listItem.findViewById(R.id.layout); + itemViewHolder.layout.setTag(R.string.position_tag, position); + itemViewHolder.layout.setOnClickListener(mRowListener); + itemViewHolder.layout.setSoundEffectsEnabled(false); + itemViewHolder.favicon = listItem.findViewById(R.id.favicon); + itemViewHolder.title = listItem.findViewById(R.id.title); + itemViewHolder.url = listItem.findViewById(R.id.url); + itemViewHolder.delete = listItem.findViewById(R.id.delete); + itemViewHolder.delete.setTag(R.string.position_tag, position); + itemViewHolder.delete.setSoundEffectsEnabled(false); + itemViewHolder.delete.setOnClickListener(mDeleteButtonListener); + itemViewHolder.divider = listItem.findViewById(R.id.divider); + + listItem.setTag(R.string.list_item_view_tag, itemViewHolder); + + listItem.setOnHoverListener(mHoverListener); + listItem.setOnTouchListener(mTouchListener); + + } else { + itemViewHolder = (ItemViewHolder) listItem.getTag(R.string.list_item_view_tag); + itemViewHolder.layout.setTag(R.string.position_tag, position); + itemViewHolder.delete.setTag(R.string.position_tag, position); + } + + SuggestionItem selectedItem = getItem(position); + + // Make search substring as bold + final SpannableStringBuilder sb = new SpannableStringBuilder(selectedItem.text); + final StyleSpan bold = new StyleSpan(Typeface.BOLD); + final StyleSpan normal = new StyleSpan(Typeface.NORMAL); + int start = selectedItem.text.indexOf(mHighlightedText); + if (start >= 0) { + int end = start + mHighlightedText.length(); + sb.setSpan(normal, 0, start, Spannable.SPAN_INCLUSIVE_INCLUSIVE); + sb.setSpan(bold, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE); + sb.setSpan(normal, end, selectedItem.text.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); + } + itemViewHolder.title.setText(sb); + + // Set the URL text + if (selectedItem.url == null) { + itemViewHolder.url.setVisibility(GONE); + + } else { + itemViewHolder.url.setVisibility(VISIBLE); + itemViewHolder.url.setText(selectedItem.url); + } + + // Set the description + if (selectedItem.faviconURL == null) { + itemViewHolder.favicon.setVisibility(GONE); + + } else { + // TODO: Load favicon + itemViewHolder.favicon.setVisibility(VISIBLE); + } + + // Type related + if (selectedItem.type == SuggestionItem.Type.SUGGESTION) { + itemViewHolder.delete.setVisibility(GONE); + itemViewHolder.divider.setVisibility(GONE); + itemViewHolder.favicon.setVisibility(VISIBLE); + itemViewHolder.favicon.setImageResource(R.drawable.ic_icon_search); + + } else if (selectedItem.type == SuggestionItem.Type.COMPLETION) { + itemViewHolder.delete.setVisibility(GONE); + itemViewHolder.divider.setVisibility(VISIBLE); + itemViewHolder.favicon.setVisibility(VISIBLE); + itemViewHolder.favicon.setImageResource(R.drawable.ic_icon_browser); + } + + return listItem; + } + + OnClickListener mDeleteButtonListener = v -> { + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + + int position = (Integer)v.getTag(R.string.position_tag); + SuggestionItem item = getItem(position); + mAdapter.remove(item); + mAdapter.notifyDataSetChanged(); + + if (mURLBarDelegate != null) { + mURLBarDelegate.OnItemDeleted(item); + } + }; + + OnClickListener mRowListener = v -> { + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + + hide(); + + requestFocus(); + requestFocusFromTouch(); + + if (mURLBarDelegate != null) { + int position = (Integer)v.getTag(R.string.position_tag); + SuggestionItem item = getItem(position); + mURLBarDelegate.OnItemClicked(item); + } + }; + + private OnTouchListener mTouchListener = (view, event) -> { + int position = (int)view.getTag(R.string.position_tag); + if (!isEnabled(position)) + return false; + + int ev = event.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_UP: + view.setPressed(false); + view.performClick(); + return true; + + case MotionEvent.ACTION_DOWN: + view.setPressed(true); + return true; + } + + return false; + }; + + private OnHoverListener mHoverListener = (view, motionEvent) -> { + int position = (int)view.getTag(R.string.position_tag); + if (!isEnabled(position)) + return false; + + View favicon = view.findViewById(R.id.favicon); + View title = view.findViewById(R.id.title); + View url = view.findViewById(R.id.url); + View delete = view.findViewById(R.id.delete); + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_HOVER_ENTER: + view.setHovered(true); + favicon.setHovered(true); + title.setHovered(true); + url.setHovered(true); + delete.setHovered(true); + return true; + + case MotionEvent.ACTION_HOVER_EXIT: + view.setHovered(false); + favicon.setHovered(false); + title.setHovered(false); + url.setHovered(false); + delete.setHovered(false); + return true; + } + + return false; + }; + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TopBarWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TopBarWidget.java index 8f6446788..018ebbb7a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TopBarWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TopBarWidget.java @@ -66,7 +66,7 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.width = WidgetPlacement.dpDimension(context, R.dimen.top_bar_width); aPlacement.height = WidgetPlacement.dpDimension(context, R.dimen.top_bar_height); // FIXME: Something wrong with the DPI ratio? Revert to top_bar_world_width when fixed - aPlacement.worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.browser_world_width) * 40/720; + aPlacement.worldWidth = WidgetPlacement.floatDimension(getContext(), R.dimen.browser_world_width) * aPlacement.width/getWorldWidth(); aPlacement.translationY = WidgetPlacement.unitFromMeters(context, R.dimen.top_bar_world_y); aPlacement.anchorX = 0.5f; aPlacement.anchorY = 0.5f; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java index 162943fee..b2acb14fd 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/UIWidget.java @@ -17,6 +17,8 @@ import android.view.ViewParent; import android.widget.FrameLayout; +import org.mozilla.vrbrowser.R; + import java.lang.reflect.Constructor; import java.util.HashMap; @@ -30,6 +32,7 @@ public interface Delegate { private UISurfaceTextureRenderer mRenderer; private SurfaceTexture mTexture; + private float mWorldWidth; protected int mHandle; protected WidgetPlacement mWidgetPlacement; protected WidgetManagerDelegate mWidgetManager; @@ -61,6 +64,7 @@ private void initialize() { initializeWidgetPlacement(mWidgetPlacement); mInitialWidth = mWidgetPlacement.width; mInitialHeight = mWidgetPlacement.height; + mWorldWidth = WidgetPlacement.pixelDimension(getContext(), R.dimen.world_width); mChildren = new HashMap<>(); mBackHandler = new Runnable() { @@ -213,14 +217,20 @@ public void toggle() { } public void show() { + show(true); + } + + public void show(boolean focus) { if (!mWidgetPlacement.visible) { mWidgetPlacement.visible = true; mWidgetManager.addWidget(this); mWidgetManager.pushBackHandler(mBackHandler); } - setFocusableInTouchMode(true); - requestFocusFromTouch(); + if (focus) { + setFocusableInTouchMode(true); + requestFocusFromTouch(); + } } public void hide() { @@ -283,4 +293,8 @@ protected void onDismiss() { mDelegate.onDismiss(); } } + + protected float getWorldWidth() { + return mWorldWidth; + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java index 9bb13fb9f..2f21fb147 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetPlacement.java @@ -6,6 +6,8 @@ package org.mozilla.vrbrowser.ui.widgets; import android.content.Context; +import android.content.res.Resources; +import android.util.DisplayMetrics; import android.util.TypedValue; public class WidgetPlacement { @@ -96,4 +98,15 @@ public static float unitFromMeters(Context aContext, int aDimensionId) { return unitFromMeters(floatDimension(aContext, aDimensionId)); } + public static float convertDpToPixel(Context aContext, float dp){ + Resources resources = aContext.getResources(); + DisplayMetrics metrics = resources.getDisplayMetrics(); + float px = dp * ((float)metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); + return px; + } + + public static float convertPixelsToDp(Context aContext, float px){ + return px / ((float) aContext.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT); + } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java index 94f277d60..9e8eca9a2 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java @@ -7,6 +7,8 @@ import android.support.annotation.Nullable; +import java.util.regex.Pattern; + // This class refers from mozilla-mobile/focus-android public class UrlUtils { @@ -30,4 +32,9 @@ public static String stripCommonSubdomains(@Nullable String host) { return host.substring(start); } + + private static Pattern domainPattern = Pattern.compile("^(http:\\/\\/www\\.|https:\\/\\/www\\.|http:\\/\\/|https:\\/\\/)?[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,5}(:[0-9]{1,5})?(\\/.*)?$"); + public static boolean isDomain(String text) { + return domainPattern.matcher(text).find(); + } } diff --git a/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp b/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp index 4e2dc73b9..22e1f3695 100644 --- a/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp +++ b/app/src/googlevr/cpp/DeviceDelegateGoogleVR.cpp @@ -346,7 +346,7 @@ struct DeviceDelegateGoogleVR::State { // Handle the start of a sequence of scroll gestures. break; case GVR_GESTURE_SCROLL_UPDATE: - // Handle an update in a sequence of scroll gestures. + // Handle an initialize in a sequence of scroll gestures. break; case GVR_GESTURE_SCROLL_END: // Handle the end of a sequence of scroll gestures. diff --git a/app/src/main/cpp/moz_external_vr.h b/app/src/main/cpp/moz_external_vr.h index ca0e69622..a75c53a5a 100644 --- a/app/src/main/cpp/moz_external_vr.h +++ b/app/src/main/cpp/moz_external_vr.h @@ -143,7 +143,7 @@ enum class VRDisplayCapabilityFlags : uint16_t { * primary display. If presenting VR content will obscure * other content on the device, this should be un-set. When * un-set, the application should not attempt to mirror VR content - * or update non-VR UI because that content will not be visible. + * or initialize non-VR UI because that content will not be visible. */ Cap_External = 1 << 4, /** diff --git a/app/src/main/res/anim/popup_scaledown.xml b/app/src/main/res/anim/popup_scaledown.xml new file mode 100644 index 000000000..84119ef6b --- /dev/null +++ b/app/src/main/res/anim/popup_scaledown.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/popup_scaleup.xml b/app/src/main/res/anim/popup_scaleup.xml new file mode 100644 index 000000000..de5e60773 --- /dev/null +++ b/app/src/main/res/anim/popup_scaleup.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/fast_scroll_thumb.xml b/app/src/main/res/drawable/fast_scroll_thumb.xml new file mode 100644 index 000000000..2720ed110 --- /dev/null +++ b/app/src/main/res/drawable/fast_scroll_thumb.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_icon_browser.xml b/app/src/main/res/drawable/ic_icon_browser.xml new file mode 100644 index 000000000..efa59e09e --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_browser.xml @@ -0,0 +1,47 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_icon_search.xml b/app/src/main/res/drawable/ic_icon_search.xml new file mode 100644 index 000000000..8439630d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_search.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/suggestion_background_color.xml b/app/src/main/res/drawable/suggestion_background_color.xml new file mode 100644 index 000000000..957a1395c --- /dev/null +++ b/app/src/main/res/drawable/suggestion_background_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/suggestion_description_color.xml b/app/src/main/res/drawable/suggestion_description_color.xml new file mode 100644 index 000000000..577450005 --- /dev/null +++ b/app/src/main/res/drawable/suggestion_description_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/suggestion_icon_color.xml b/app/src/main/res/drawable/suggestion_icon_color.xml new file mode 100644 index 000000000..0eac69513 --- /dev/null +++ b/app/src/main/res/drawable/suggestion_icon_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/suggestion_text_color.xml b/app/src/main/res/drawable/suggestion_text_color.xml new file mode 100644 index 000000000..5ec95bdf3 --- /dev/null +++ b/app/src/main/res/drawable/suggestion_text_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/choice_prompt.xml b/app/src/main/res/layout/choice_prompt.xml index 0aa965e1d..763589c5c 100644 --- a/app/src/main/res/layout/choice_prompt.xml +++ b/app/src/main/res/layout/choice_prompt.xml @@ -9,10 +9,6 @@ android:id="@+id/choiceslist" android:layout_width="0dp" android:layout_height="0dp" - android:scrollbarThumbVertical="@drawable/scrollbar_thumb" - android:scrollbars="vertical" - android:fadeScrollbars="false" - android:scrollbarStyle="insideInset" android:divider="@android:color/transparent" android:dividerHeight="4dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="20dp" diff --git a/app/src/main/res/layout/list_popup_window.xml b/app/src/main/res/layout/list_popup_window.xml new file mode 100644 index 000000000..c3f67e3da --- /dev/null +++ b/app/src/main/res/layout/list_popup_window.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_popup_window_item.xml b/app/src/main/res/layout/list_popup_window_item.xml new file mode 100644 index 000000000..3f661f612 --- /dev/null +++ b/app/src/main/res/layout/list_popup_window_item.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/navigation_url.xml b/app/src/main/res/layout/navigation_url.xml index 154354f9e..797569660 100644 --- a/app/src/main/res/layout/navigation_url.xml +++ b/app/src/main/res/layout/navigation_url.xml @@ -1,22 +1,27 @@ - - + + + android:ellipsize="none" + android:textSize="16dip" + app:autocompleteBackgroundColor="@color/azure"/> + + 800px + 800px 4.0 @@ -109,4 +112,11 @@ 14dip 12dip 10dip + + + 4.0 + 250dp + 1.3 + 0.15 + 0.2 \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 77ad32e8e..f8093cf93 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -192,6 +192,17 @@ @style/scrollbar @style/radioButtonTheme @drawable/cursor_drawable + + + true + true + 15dp + @drawable/fast_scroll_thumb + @style/Widget.FastScroll + atThumb + true +