// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.physicalweb; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.graphics.drawable.AnimationDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.ContextUtils; import org.chromium.base.Log; import org.chromium.base.VisibleForTesting; import org.chromium.chrome.R; import org.chromium.chrome.browser.widget.FadingShadow; import org.chromium.chrome.browser.widget.FadingShadowView; import org.chromium.components.location.LocationUtils; import java.util.ArrayList; import java.util.Collection; import java.util.List; /** * This activity displays a list of nearby URLs as stored in the {@link UrlManager}. * This activity does not and should not rely directly or indirectly on the native library. */ public class ListUrlsActivity extends AppCompatActivity implements AdapterView.OnItemClickListener, SwipeRefreshWidget.OnRefreshListener, UrlManager.Listener { public static final String REFERER_KEY = "referer"; public static final int NOTIFICATION_REFERER = 1; public static final int OPTIN_REFERER = 2; public static final int PREFERENCE_REFERER = 3; public static final int DIAGNOSTICS_REFERER = 4; public static final int REFERER_BOUNDARY = 5; private static final String TAG = "PhysicalWeb"; private static final String PREFS_VERSION_KEY = "org.chromium.chrome.browser.physicalweb.VERSION"; private static final String PREFS_BOTTOM_BAR_KEY = "org.chromium.chrome.browser.physicalweb.BOTTOM_BAR_DISPLAY_COUNT"; private static final int PREFS_VERSION = 1; private static final int BOTTOM_BAR_DISPLAY_LIMIT = 1; private static final int DURATION_SLIDE_UP_MS = 250; private static final int DURATION_SLIDE_DOWN_MS = 250; private final List<PwsResult> mPwsResults = new ArrayList<>(); private Context mContext; private NearbyUrlsAdapter mAdapter; private PwsClient mPwsClient; private ListView mListView; private TextView mEmptyListText; private ImageView mScanningImageView; private SwipeRefreshWidget mSwipeRefreshWidget; private View mBottomBar; private boolean mIsInitialDisplayRecorded; private boolean mIsRefreshing; private boolean mIsRefreshUserInitiated; private NearbyForegroundSubscription mNearbyForegroundSubscription; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mContext = this; setContentView(R.layout.physical_web_list_urls_activity); initSharedPreferences(); mAdapter = new NearbyUrlsAdapter(this); View emptyView = findViewById(R.id.physical_web_empty); mListView = (ListView) findViewById(R.id.physical_web_urls_list); mListView.setEmptyView(emptyView); mListView.setAdapter(mAdapter); mListView.setOnItemClickListener(this); mEmptyListText = (TextView) findViewById(R.id.physical_web_empty_list_text); mScanningImageView = (ImageView) findViewById(R.id.physical_web_logo); mSwipeRefreshWidget = (SwipeRefreshWidget) findViewById(R.id.physical_web_swipe_refresh_widget); mSwipeRefreshWidget.setOnRefreshListener(this); mBottomBar = findViewById(R.id.physical_web_bottom_bar); int shadowColor = ApiCompatibilityUtils.getColor(getResources(), R.color.bottom_bar_shadow_color); FadingShadowView shadow = (FadingShadowView) findViewById(R.id.physical_web_bottom_bar_shadow); shadow.init(shadowColor, FadingShadow.POSITION_BOTTOM); View bottomBarClose = (View) findViewById(R.id.physical_web_bottom_bar_close); bottomBarClose.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { hideBottomBar(); } }); mPwsClient = new PwsClientImpl(this); int referer = getIntent().getIntExtra(REFERER_KEY, 0); if (savedInstanceState == null) { // Ensure this is a newly-created activity. PhysicalWebUma.onActivityReferral(this, referer); } mIsInitialDisplayRecorded = false; mIsRefreshing = false; mIsRefreshUserInitiated = false; mNearbyForegroundSubscription = new NearbyForegroundSubscription(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { int tintColor = ContextCompat.getColor(this, R.color.light_normal_color); Drawable tintedRefresh = ContextCompat.getDrawable(this, R.drawable.btn_toolbar_reload); tintedRefresh.setColorFilter(tintColor, PorterDuff.Mode.SRC_IN); menu.add(0, R.id.menu_id_refresh, 1, R.string.physical_web_refresh) .setIcon(tintedRefresh) .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS); menu.add(0, R.id.menu_id_close, 2, R.string.close) .setIcon(R.drawable.btn_close) .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.menu_id_close) { finish(); return true; } else if (id == R.id.menu_id_refresh) { startRefresh(true, false); return true; } Log.e(TAG, "Unknown menu item selected"); return super.onOptionsItemSelected(item); } @Override protected void onStart() { super.onStart(); UrlManager.getInstance().addObserver(this); // Only connect so that we can subscribe to Nearby if we have the location permission. LocationUtils locationUtils = LocationUtils.getInstance(); if (locationUtils.hasAndroidLocationPermission() && locationUtils.isSystemLocationSettingEnabled()) { mNearbyForegroundSubscription.connect(); } } @Override protected void onResume() { super.onResume(); mNearbyForegroundSubscription.subscribe(); startRefresh(false, false); int bottomBarDisplayCount = getBottomBarDisplayCount(); if (bottomBarDisplayCount < BOTTOM_BAR_DISPLAY_LIMIT) { showBottomBar(); setBottomBarDisplayCount(bottomBarDisplayCount + 1); } } @Override protected void onPause() { mNearbyForegroundSubscription.unsubscribe(); super.onPause(); } @Override public void onRefresh() { startRefresh(true, true); } @Override protected void onStop() { UrlManager.getInstance().removeObserver(this); mNearbyForegroundSubscription.disconnect(); super.onStop(); } private void resolve(Collection<UrlInfo> urls, final boolean isUserInitiated) { final long timestamp = SystemClock.elapsedRealtime(); mPwsClient.resolve(urls, new PwsClient.ResolveScanCallback() { @Override public void onPwsResults(Collection<PwsResult> pwsResults) { long duration = SystemClock.elapsedRealtime() - timestamp; if (isUserInitiated) { PhysicalWebUma.onRefreshPwsResolution(ListUrlsActivity.this, duration); } else { PhysicalWebUma.onForegroundPwsResolution(ListUrlsActivity.this, duration); } // filter out duplicate groups. for (PwsResult pwsResult : pwsResults) { mPwsResults.add(pwsResult); if (!mAdapter.hasGroupId(pwsResult.groupId)) { mAdapter.add(pwsResult); if (pwsResult.iconUrl != null && !mAdapter.hasIcon(pwsResult.iconUrl)) { fetchIcon(pwsResult.iconUrl); } } } finishRefresh(); } }); } /** * Handle a click event. * @param adapterView The AdapterView where the click happened. * @param view The View that was clicked inside the AdapterView. * @param position The position of the clicked element in the list. * @param id The row id of the clicked element in the list. */ @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { PhysicalWebUma.onUrlSelected(this); PwsResult minPwsResult = mAdapter.getItem(position); String groupId = minPwsResult.groupId; // Make sure the PwsResult corresponds to the closest UrlDevice in the group. double minDistance = Double.MAX_VALUE; for (PwsResult pwsResult : mPwsResults) { if (pwsResult.groupId.equals(groupId)) { double distance = UrlManager.getInstance() .getUrlInfoByUrl(pwsResult.requestUrl).getDistance(); if (distance < minDistance) { minDistance = distance; minPwsResult = pwsResult; } } } Intent intent = createNavigateToUrlIntent(minPwsResult); mContext.startActivity(intent); } /** * Called when new nearby URLs are found. * @param urls The set of newly-found nearby URLs. */ @Override public void onDisplayableUrlsAdded(Collection<UrlInfo> urls) { resolve(urls, false); } private void startRefresh(boolean isUserInitiated, boolean isSwipeInitiated) { if (mIsRefreshing) { return; } mIsRefreshing = true; mIsRefreshUserInitiated = isUserInitiated; // Clear the list adapter to trigger the empty list display. mAdapter.clear(); Collection<UrlInfo> urls = UrlManager.getInstance().getUrls(true); // Check the Physical Web preference to ensure we do not resolve URLs when Physical Web is // off or onboarding. Normally the user will not reach this activity unless the preference // is explicitly enabled, but there is a button on the diagnostics page that launches into // the activity without checking the preference state. if (urls.isEmpty() || !PhysicalWeb.isPhysicalWebPreferenceEnabled()) { finishRefresh(); } else { // Show the swipe-to-refresh busy indicator for refreshes initiated by a swipe. if (isSwipeInitiated) { mSwipeRefreshWidget.setRefreshing(true); } // Update the empty list view to show a scanning animation. mEmptyListText.setText(R.string.physical_web_empty_list_scanning); mScanningImageView.setImageResource(R.drawable.physical_web_scanning_animation); mScanningImageView.setColorFilter(null); AnimationDrawable animationDrawable = (AnimationDrawable) mScanningImageView.getDrawable(); animationDrawable.start(); mPwsResults.clear(); resolve(urls, isUserInitiated); } } private void finishRefresh() { // Hide the busy indicator. mSwipeRefreshWidget.setRefreshing(false); // Stop the scanning animation, show a "nothing found" message. mEmptyListText.setText(R.string.physical_web_empty_list); int tintColor = ContextCompat.getColor(this, R.color.physical_web_logo_gray_tint); mScanningImageView.setImageResource(R.drawable.physical_web_logo); mScanningImageView.setColorFilter(tintColor, PorterDuff.Mode.SRC_IN); // Record refresh-related UMA. if (!mIsInitialDisplayRecorded) { mIsInitialDisplayRecorded = true; PhysicalWebUma.onUrlsDisplayed(this, mAdapter.getCount()); } else if (mIsRefreshUserInitiated) { PhysicalWebUma.onUrlsRefreshed(this, mAdapter.getCount()); } mIsRefreshing = false; } private void fetchIcon(String iconUrl) { mPwsClient.fetchIcon(iconUrl, new PwsClient.FetchIconCallback() { @Override public void onIconReceived(String url, Bitmap bitmap) { mAdapter.setIcon(url, bitmap); } }); } private void showBottomBar() { mBottomBar.setTranslationY(mBottomBar.getHeight()); mBottomBar.setVisibility(View.VISIBLE); Animator animator = createTranslationYAnimator(mBottomBar, 0f, DURATION_SLIDE_UP_MS); animator.start(); } private void hideBottomBar() { Animator animator = createTranslationYAnimator(mBottomBar, mBottomBar.getHeight(), DURATION_SLIDE_DOWN_MS); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mBottomBar.setVisibility(View.GONE); } }); animator.start(); } private static Animator createTranslationYAnimator(View view, float endValue, long durationMillis) { return ObjectAnimator.ofFloat(view, "translationY", view.getTranslationY(), endValue) .setDuration(durationMillis); } private void initSharedPreferences() { if (ContextUtils.getAppSharedPreferences().getInt(PREFS_VERSION_KEY, 0) == PREFS_VERSION) { return; } // Stored preferences are old, upgrade to the current version. ContextUtils.getAppSharedPreferences().edit() .putInt(PREFS_VERSION_KEY, PREFS_VERSION) .apply(); } private int getBottomBarDisplayCount() { return ContextUtils.getAppSharedPreferences().getInt(PREFS_BOTTOM_BAR_KEY, 0); } private void setBottomBarDisplayCount(int count) { ContextUtils.getAppSharedPreferences().edit() .putInt(PREFS_BOTTOM_BAR_KEY, count) .apply(); } private static Intent createNavigateToUrlIntent(PwsResult pwsResult) { String url = pwsResult.siteUrl; if (url == null) { url = pwsResult.requestUrl; } return new Intent(Intent.ACTION_VIEW) .addCategory(Intent.CATEGORY_BROWSABLE) .setData(Uri.parse(url)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } @VisibleForTesting void overridePwsClientForTesting(PwsClient pwsClient) { mPwsClient = pwsClient; } @VisibleForTesting void overrideContextForTesting(Context context) { mContext = context; } }