package org.wikipedia.main; import android.annotation.SuppressLint; import android.app.Activity; import android.app.DownloadManager; import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.location.Location; import android.os.Build; import android.os.Bundle; import android.speech.RecognizerIntent; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.BottomNavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.ViewPager; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import org.apache.commons.lang3.StringUtils; import org.wikipedia.BackPressedHandler; import org.wikipedia.Constants; import org.wikipedia.R; import org.wikipedia.WikipediaApp; import org.wikipedia.activity.FragmentUtil; import org.wikipedia.analytics.GalleryFunnel; import org.wikipedia.analytics.IntentFunnel; import org.wikipedia.analytics.LoginFunnel; import org.wikipedia.feed.FeedFragment; import org.wikipedia.feed.featured.FeaturedArticleCard; import org.wikipedia.feed.image.FeaturedImage; import org.wikipedia.feed.image.FeaturedImageCard; import org.wikipedia.feed.model.Card; import org.wikipedia.feed.news.NewsActivity; import org.wikipedia.feed.news.NewsItemCard; import org.wikipedia.gallery.GalleryActivity; import org.wikipedia.gallery.ImagePipelineBitmapGetter; import org.wikipedia.gallery.MediaDownloadReceiver; import org.wikipedia.history.HistoryEntry; import org.wikipedia.history.HistoryFragment; import org.wikipedia.login.LoginActivity; import org.wikipedia.navtab.NavTab; import org.wikipedia.navtab.NavTabFragmentPagerAdapter; import org.wikipedia.navtab.NavTabLayout; import org.wikipedia.nearby.NearbyFragment; import org.wikipedia.page.ExclusiveBottomSheetPresenter; import org.wikipedia.page.PageActivity; import org.wikipedia.page.PageTitle; import org.wikipedia.page.linkpreview.LinkPreviewDialog; import org.wikipedia.readinglist.AddToReadingListDialog; import org.wikipedia.readinglist.sync.ReadingListSynchronizer; import org.wikipedia.search.SearchFragment; import org.wikipedia.search.SearchInvokeSource; import org.wikipedia.settings.Prefs; import org.wikipedia.util.ClipboardUtil; import org.wikipedia.util.FeedbackUtil; import org.wikipedia.util.PermissionUtil; import org.wikipedia.util.ShareUtil; import org.wikipedia.util.log.L; import java.io.File; import java.util.concurrent.TimeUnit; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnPageChange; import butterknife.Unbinder; public class MainFragment extends Fragment implements BackPressedHandler, FeedFragment.Callback, NearbyFragment.Callback, HistoryFragment.Callback, SearchFragment.Callback, LinkPreviewDialog.Callback { @BindView(R.id.fragment_main_view_pager) ViewPager viewPager; @BindView(R.id.fragment_main_nav_tab_layout) NavTabLayout tabLayout; private Unbinder unbinder; private ExclusiveBottomSheetPresenter bottomSheetPresenter = new ExclusiveBottomSheetPresenter(); private MediaDownloadReceiver mediaDownloadReceiver; // The permissions request API doesn't take a callback, so in the event we have to // ask for permission to download a featured image from the feed, we'll have to hold // the image we're waiting for permission to download as a bit of state here. :( @Nullable private FeaturedImage pendingDownloadImage; public interface Callback { void onTabChanged(@NonNull NavTab tab); void onSearchOpen(); void onSearchClose(boolean shouldFinishActivity); @Nullable View getOverflowMenuAnchor(); } public static MainFragment newInstance() { MainFragment fragment = new MainFragment(); fragment.setRetainInstance(true); return fragment; } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View view = inflater.inflate(R.layout.fragment_main, container, false); unbinder = ButterKnife.bind(this, view); viewPager.setAdapter(new NavTabFragmentPagerAdapter(getChildFragmentManager())); tabLayout.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { viewPager.setCurrentItem(item.getOrder()); return true; } }); ReadingListSynchronizer.instance().sync(); if (savedInstanceState == null) { handleIntent(getActivity().getIntent()); } return view; } @Override public void onPause() { super.onPause(); getContext().unregisterReceiver(mediaDownloadReceiver); } @Override public void onResume() { super.onResume(); getContext().registerReceiver(mediaDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); // update toolbar, since Tab count might have changed getActivity().supportInvalidateOptionsMenu(); // reset the last-page-viewed timer Prefs.pageLastShown(0); } @Override public void onDestroyView() { unbinder.unbind(); unbinder = null; super.onDestroyView(); } @Override public void onActivityResult(int requestCode, int resultCode, final Intent data) { if (requestCode == Constants.ACTIVITY_REQUEST_VOICE_SEARCH && resultCode == Activity.RESULT_OK && data != null && data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) != null) { String searchQuery = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS).get(0); openSearchFragment(SearchInvokeSource.VOICE, searchQuery); } else if (requestCode == Constants.ACTIVITY_REQUEST_GALLERY && resultCode == GalleryActivity.ACTIVITY_RESULT_PAGE_SELECTED) { startActivity(data); } else if (requestCode == Constants.ACTIVITY_REQUEST_LOGIN && resultCode == LoginActivity.RESULT_LOGIN_SUCCESS) { FeedbackUtil.showMessage(this, R.string.login_success_toast); } else { super.onActivityResult(requestCode, resultCode, data); } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(true); mediaDownloadReceiver = new MediaDownloadReceiver(getActivity()); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case Constants.ACTIVITY_REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION: if (PermissionUtil.isPermitted(grantResults)) { if (pendingDownloadImage != null) { download(pendingDownloadImage); } } else { setPendingDownload(null); L.i("Write permission was denied by user"); FeedbackUtil.showMessage(this, R.string.gallery_save_image_write_permission_rationale); } break; default: super.onRequestPermissionsResult(requestCode, permissions, grantResults); } } public void handleIntent(Intent intent) { IntentFunnel funnel = new IntentFunnel(WikipediaApp.getInstance()); if (Intent.ACTION_SEND.equals(intent.getAction()) && Constants.PLAIN_TEXT_MIME_TYPE.equals(intent.getType())) { funnel.logShareIntent(); openSearchFragment(SearchInvokeSource.INTENT_SHARE, intent.getStringExtra(Intent.EXTRA_TEXT)); } else if (Intent.ACTION_PROCESS_TEXT.equals(intent.getAction()) && Constants.PLAIN_TEXT_MIME_TYPE.equals(intent.getType()) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { funnel.logProcessTextIntent(); openSearchFragment(SearchInvokeSource.INTENT_PROCESS_TEXT, intent.getStringExtra(Intent.EXTRA_PROCESS_TEXT)); } else if (intent.hasExtra(Constants.INTENT_SEARCH_FROM_WIDGET)) { funnel.logSearchWidgetTap(); openSearchFragment(SearchInvokeSource.WIDGET, null); } else if (intent.hasExtra(Constants.INTENT_EXTRA_DELETE_READING_LIST)) { goToTab(NavTab.READING_LISTS); } else if (lastPageViewedWithin(1) && !intent.hasExtra(Constants.INTENT_RETURN_TO_MAIN)) { startActivity(PageActivity.newIntent(getContext())); } } @Override public void onFeedTabListRequested() { startActivity(PageActivity.newIntentForTabList(getContext())); } @Override public void onFeedSearchRequested() { openSearchFragment(SearchInvokeSource.FEED_BAR, null); } @Override public void onFeedVoiceSearchRequested() { Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); try { startActivityForResult(intent, Constants.ACTIVITY_REQUEST_VOICE_SEARCH); } catch (ActivityNotFoundException a) { FeedbackUtil.showMessage(this, R.string.error_voice_search_not_available); } } @Override public void onFeedSelectPage(HistoryEntry entry) { startActivity(PageActivity.newIntent(getContext(), entry, entry.getTitle())); } @Override public void onFeedAddPageToList(HistoryEntry entry) { bottomSheetPresenter.show(getChildFragmentManager(), AddToReadingListDialog.newInstance(entry.getTitle(), AddToReadingListDialog.InvokeSource.FEED)); } @Override public void onFeedAddFeaturedPageToList(@NonNull final FeedFragment fragment, @NonNull final FeaturedArticleCard card, @NonNull HistoryEntry entry) { bottomSheetPresenter.show(getChildFragmentManager(), AddToReadingListDialog.newInstance(entry.getTitle(), AddToReadingListDialog.InvokeSource.FEED, new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialogInterface) { // Update card view in case saved state has changed fragment.notifyItemChanged(card); } })); } @Override public void onFeedRemovePageFromList(FeedFragment fragment, Card card, HistoryEntry entry) { FeedbackUtil.showMessage(getActivity(), getString(R.string.reading_list_item_deleted, entry.getTitle().getDisplayText())); fragment.notifyItemChanged(card); } @Override public void onFeedSharePage(HistoryEntry entry) { ShareUtil.shareText(getContext(), entry.getTitle()); } @Override public void onFeedNewsItemSelected(NewsItemCard card) { startActivity(NewsActivity.newIntent(getContext(), card.item(), card.wikiSite())); } @Override public void onFeedShareImage(final FeaturedImageCard card) { final String thumbUrl = card.baseImage().thumbnail().source().toString(); final String fullSizeUrl = card.baseImage().image().source().toString(); new ImagePipelineBitmapGetter(getContext(), thumbUrl) { @Override public void onSuccess(@Nullable Bitmap bitmap) { if (bitmap != null) { ShareUtil.shareImage(getContext(), bitmap, new File(thumbUrl).getName(), ShareUtil.getFeaturedImageShareSubject(getContext(), card.age()), fullSizeUrl); } else { FeedbackUtil.showMessage(MainFragment.this, getString(R.string.gallery_share_error, card.baseImage().title())); } } }.get(); } @Override public void onFeedDownloadImage(FeaturedImage image) { if (!(PermissionUtil.hasWriteExternalStoragePermission(getContext()))) { setPendingDownload(image); requestWriteExternalStoragePermission(); } else { download(image); } } @Override public void onFeaturedImageSelected(FeaturedImageCard card) { startActivityForResult(GalleryActivity.newIntent(getActivity(), card.age(), card.filename(), card.baseImage(), card.wikiSite(), GalleryFunnel.SOURCE_FEED_FEATURED_IMAGE), Constants.ACTIVITY_REQUEST_GALLERY); } @Override public void onLoginRequested() { startActivityForResult(LoginActivity.newIntent(getContext(), LoginFunnel.SOURCE_NAV), Constants.ACTIVITY_REQUEST_LOGIN); } @NonNull @Override public View getOverflowMenuAnchor() { Callback callback = callback(); return callback == null ? viewPager : callback.getOverflowMenuAnchor(); } @Override public void onLoading() { // todo: [overhaul] update loading indicator. } @Override public void onLoaded() { // todo: [overhaul] update loading indicator. } @Override public void onLoadPage(PageTitle title, int entrySource, @Nullable Location location) { showLinkPreview(title, entrySource, location); } @Override public void onLoadPage(PageTitle title, HistoryEntry entry) { startActivity(PageActivity.newIntent(getContext(), entry, entry.getTitle())); } @Override public void onClearHistory() { // todo: [overhaul] clear history. } @Override public void onSearchResultCopyLink(@NonNull PageTitle title) { copyLink(title.getCanonicalUri()); } @Override public void onSearchResultAddToList(@NonNull PageTitle title, @NonNull AddToReadingListDialog.InvokeSource source) { bottomSheetPresenter.show(getChildFragmentManager(), AddToReadingListDialog.newInstance(title, source)); } @Override public void onSearchResultShareLink(@NonNull PageTitle title) { ShareUtil.shareText(getContext(), title); } @Override public void onSearchSelectPage(@NonNull HistoryEntry entry, boolean inNewTab) { startActivity(PageActivity.newIntent(getContext(), entry, entry.getTitle())); } @Override public void onSearchOpen() { Callback callback = callback(); if (callback != null) { callback.onSearchOpen(); } } @Override public void onSearchClose(boolean launchedFromIntent) { SearchFragment fragment = searchFragment(); if (fragment != null) { closeSearchFragment(fragment); } Callback callback = callback(); if (callback != null) { callback.onSearchClose(launchedFromIntent); } } @Override public void onLinkPreviewLoadPage(@NonNull PageTitle title, @NonNull HistoryEntry entry, boolean inNewTab) { startActivity(PageActivity.newIntent(getContext(), entry, entry.getTitle())); } @Override public void onLinkPreviewCopyLink(@NonNull PageTitle title) { copyLink(title.getCanonicalUri()); } @Override public void onLinkPreviewAddToList(@NonNull PageTitle title) { bottomSheetPresenter.show(getChildFragmentManager(), AddToReadingListDialog.newInstance(title, AddToReadingListDialog.InvokeSource.LINK_PREVIEW_MENU)); } @Override public void onLinkPreviewShareLink(@NonNull PageTitle title) { ShareUtil.shareText(getContext(), title); } @Override public boolean onBackPressed() { SearchFragment searchFragment = searchFragment(); if (searchFragment != null && searchFragment.onBackPressed()) { return true; } Fragment fragment = ((NavTabFragmentPagerAdapter) viewPager.getAdapter()).getCurrentFragment(); if (fragment instanceof BackPressedHandler && ((BackPressedHandler) fragment).onBackPressed()) { return true; } return false; } public void setBottomNavVisible(boolean visible) { tabLayout.setVisibility(visible ? View.VISIBLE : View.GONE); } @OnPageChange(R.id.fragment_main_view_pager) void onTabChanged(int position) { Callback callback = callback(); if (callback != null) { NavTab tab = NavTab.of(position); callback.onTabChanged(tab); } } private void showLinkPreview(PageTitle title, int entrySource, @Nullable Location location) { bottomSheetPresenter.show(getChildFragmentManager(), LinkPreviewDialog.newInstance(title, entrySource, location)); } private void copyLink(@NonNull String url) { ClipboardUtil.setPlainText(getContext(), null, url); FeedbackUtil.showMessage(this, R.string.address_copied); } private boolean lastPageViewedWithin(int days) { return TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - Prefs.pageLastShown()) < days; } private void download(@NonNull FeaturedImage image) { setPendingDownload(null); new MediaDownloadReceiver(getActivity()).download(image); } private void setPendingDownload(@Nullable FeaturedImage image) { pendingDownloadImage = image; } private void requestWriteExternalStoragePermission() { PermissionUtil.requestWriteStorageRuntimePermissions(this, Constants.ACTIVITY_REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION); } @SuppressLint("CommitTransaction") private void openSearchFragment(@NonNull SearchInvokeSource source, @Nullable String query) { Fragment fragment = searchFragment(); if (fragment == null) { fragment = SearchFragment.newInstance(source, StringUtils.trim(query), false); getChildFragmentManager() .beginTransaction() .add(R.id.fragment_main_container, fragment) .commitNowAllowingStateLoss(); } } @SuppressLint("CommitTransaction") private void closeSearchFragment(@NonNull SearchFragment fragment) { getChildFragmentManager().beginTransaction().remove(fragment).commitNowAllowingStateLoss(); } @Nullable private SearchFragment searchFragment() { return (SearchFragment) getChildFragmentManager().findFragmentById(R.id.fragment_main_container); } private void cancelSearch() { SearchFragment fragment = searchFragment(); if (fragment != null) { fragment.closeSearch(); } } private void goToTab(@NonNull NavTab tab) { tabLayout.setSelectedItemId(tab.code()); cancelSearch(); } @Nullable private Callback callback() { return FragmentUtil.getCallback(this, Callback.class); } }