Skip to content

Commit

Permalink
Merge pull request #3 from shiki/issue/8627-giphy-picker-media-lib-im…
Browse files Browse the repository at this point in the history
…port

Import from Giphy in Media Library
  • Loading branch information
kwonye committed Dec 13, 2018
2 parents b855d9a + 9f01136 commit 316fb59
Show file tree
Hide file tree
Showing 15 changed files with 559 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.wordpress.android.viewmodel.ViewModelKey;
import org.wordpress.android.viewmodel.activitylog.ActivityLogDetailViewModel;
import org.wordpress.android.viewmodel.activitylog.ActivityLogViewModel;
import org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel;
import org.wordpress.android.viewmodel.history.HistoryViewModel;
import org.wordpress.android.viewmodel.pages.PageListViewModel;
import org.wordpress.android.viewmodel.pages.PageParentViewModel;
Expand Down Expand Up @@ -84,6 +85,11 @@ abstract class ViewModelModule {
@ViewModelKey(PostListViewModel.class)
abstract ViewModel postListViewModel(PostListViewModel viewModel);

@Binds
@IntoMap
@ViewModelKey(GiphyPickerViewModel.class)
abstract ViewModel giphyPickerViewModel(GiphyPickerViewModel viewModel);

@Binds
abstract ViewModelProvider.Factory provideViewModelFactory(ViewModelFactory viewModelFactory);
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,13 @@ public static void showStockMediaPickerForResult(Activity activity,
}

public static void showGiphyPickerForResult(Activity activity, @NonNull SiteModel site, int requestCode) {
Map<String, String> properties = new HashMap<>();
properties.put("from", activity.getClass().getSimpleName());
AnalyticsTracker.track(AnalyticsTracker.Stat.GIPHY_PICKER_ACCESSED, properties);

Intent intent = new Intent(activity, GiphyPickerActivity.class);
intent.putExtra(WordPress.SITE, site);

activity.startActivityForResult(intent, requestCode);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
package org.wordpress.android.ui.giphy

import android.app.Activity
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.SearchView
import android.support.v7.widget.SearchView.OnQueryTextListener
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RelativeLayout
import kotlinx.android.synthetic.main.media_picker_activity.*
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.analytics.AnalyticsTracker
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.giphy.GiphyMediaViewHolder.ThumbnailViewDimensions
import org.wordpress.android.util.AniUtils
import org.wordpress.android.util.DisplayUtils
import org.wordpress.android.util.ToastUtils
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.viewmodel.ViewModelFactory
import org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel
import org.wordpress.android.viewmodel.giphy.GiphyPickerViewModel.State
import javax.inject.Inject

/**
Expand All @@ -28,6 +37,7 @@ class GiphyPickerActivity : AppCompatActivity() {
* Used for loading images in [GiphyMediaViewHolder]
*/
@Inject lateinit var imageManager: ImageManager
@Inject lateinit var viewModelFactory: ViewModelFactory

private lateinit var viewModel: GiphyPickerViewModel

Expand All @@ -45,7 +55,10 @@ class GiphyPickerActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
(application as WordPress).component().inject(this)

viewModel = ViewModelProviders.of(this).get(GiphyPickerViewModel::class.java)
val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel

viewModel = ViewModelProviders.of(this, viewModelFactory).get(GiphyPickerViewModel::class.java)
viewModel.setup(site)

// We are intentionally reusing this layout since the UI is very similar.
setContentView(R.layout.media_picker_activity)
Expand All @@ -54,6 +67,8 @@ class GiphyPickerActivity : AppCompatActivity() {
initializeRecyclerView()
initializeSearchView()
initializeSelectionBar()
initializeDownloadHandlers()
initializeStateChangeHandlers()
}

/**
Expand Down Expand Up @@ -148,6 +163,73 @@ class GiphyPickerActivity : AppCompatActivity() {
})
}

/**
* Set up reacting to "Add" button presses and processing the result
*/
private fun initializeDownloadHandlers() {
text_add.setOnClickListener { viewModel.downloadSelected() }

viewModel.downloadResult.observe(this, Observer { result ->
if (result?.mediaModels != null) {
val mediaLocalIds = result.mediaModels.map { it.id }.toIntArray()

trackDownloadedMedia(mediaLocalIds)

val intent = Intent().apply { putExtra(KEY_SAVED_MEDIA_MODEL_LOCAL_IDS, mediaLocalIds) }
setResult(Activity.RESULT_OK, intent)
finish()
} else if (result?.errorMessageStringResId != null) {
ToastUtils.showToast(
this@GiphyPickerActivity,
result.errorMessageStringResId,
ToastUtils.Duration.SHORT
)
}
})
}

/**
* Set up enabling/disabling of controls depending on the current [GiphyPickerViewModel.State]:
*
* - [State.IDLE]: All normal functions are allowed
* - [State.DOWNLOADING] or [State.FINISHED]: "Add", "Preview", searching, and selecting are disabled
* - [State.DOWNLOADING]: The "Add" button is replaced with a progress bar
*/
private fun initializeStateChangeHandlers() {
viewModel.state.observe(this, Observer { state ->
state ?: return@Observer

val searchClearButton =
search_view.findViewById(android.support.v7.appcompat.R.id.search_close_btn) as ImageView
val searchEditText =
search_view.findViewById(android.support.v7.appcompat.R.id.search_src_text)
as SearchView.SearchAutoComplete

val isIdle = state == State.IDLE
val isDownloading = state == State.DOWNLOADING

// Disable all the controls if we are not idle
text_add.isEnabled = isIdle
text_preview.isEnabled = isIdle
searchClearButton.isEnabled = isIdle
searchEditText.isEnabled = isIdle

// Show the progress bar instead of the Add text if we are downloading
upload_progress.visibility = if (isDownloading) View.VISIBLE else View.GONE
// The Add text should not be View.GONE because the progress bar relies on its layout to position itself
text_add.visibility = if (isDownloading) View.INVISIBLE else View.VISIBLE
})
}

private fun trackDownloadedMedia(mediaLocalIds: IntArray) {
if (mediaLocalIds.isEmpty()) {
return
}

val properties = mapOf("number_of_media_selected" to mediaLocalIds.size)
AnalyticsTracker.track(AnalyticsTracker.Stat.GIPHY_PICKER_DOWNLOADED, properties)
}

/**
* Close this Activity when the up button is pressed
*/
Expand All @@ -159,4 +241,11 @@ class GiphyPickerActivity : AppCompatActivity() {

return super.onOptionsItemSelected(item)
}

companion object {
/**
* Added to this Activity's result as an Int array [org.wordpress.android.fluxc.model.MediaModel] `id` values.
*/
const val KEY_SAVED_MEDIA_MODEL_LOCAL_IDS = "saved_media_model_local_ids"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import org.wordpress.android.ui.ActivityId;
import org.wordpress.android.ui.ActivityLauncher;
import org.wordpress.android.ui.RequestCodes;
import org.wordpress.android.ui.giphy.GiphyPickerActivity;
import org.wordpress.android.ui.media.MediaGridFragment.MediaFilter;
import org.wordpress.android.ui.media.MediaGridFragment.MediaGridListener;
import org.wordpress.android.ui.media.services.MediaDeleteService;
Expand Down Expand Up @@ -472,6 +473,17 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
reloadMediaGrid();
}
break;
case RequestCodes.GIPHY_PICKER:
if (resultCode == RESULT_OK && data.hasExtra(GiphyPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS)) {
int[] mediaLocalIds = data.getIntArrayExtra(GiphyPickerActivity.KEY_SAVED_MEDIA_MODEL_LOCAL_IDS);
ArrayList<MediaModel> mediaModels = new ArrayList<>();
for (int localId : mediaLocalIds) {
mediaModels.add(mMediaStore.getMediaWithLocalId(localId));
}

addMediaToUploadService(mediaModels);
}
break;
}
}

Expand Down Expand Up @@ -978,16 +990,21 @@ private Uri getOptimizedPictureIfNecessary(Uri originalUri) {
}

private void addMediaToUploadService(@NonNull MediaModel media) {
ArrayList<MediaModel> mediaList = new ArrayList<>();
mediaList.add(media);

addMediaToUploadService(mediaList);
}

private void addMediaToUploadService(@NonNull ArrayList<MediaModel> mediaModels) {
// Start the upload service if it's not started and fill the media queue
if (!NetworkUtils.isNetworkAvailable(this)) {
AppLog.v(AppLog.T.MEDIA, "Unable to start UploadService, internet connection required.");
ToastUtils.showToast(this, R.string.no_network_message, ToastUtils.Duration.SHORT);
return;
}

ArrayList<MediaModel> mediaList = new ArrayList<>();
mediaList.add(media);
UploadService.uploadMedia(this, mediaList);
UploadService.uploadMedia(this, mediaModels);
}

private void queueFileForUpload(Uri uri, String mimeType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,29 +420,51 @@ public interface MediaFetchDoNext {
void doNext(Uri uri);
}

public static boolean fetchMediaAndDoNext(Context context, Uri mediaUri, MediaFetchDoNext listener) {
if (!MediaUtils.isInMediaStore(mediaUri)) {
/**
* Downloads the {@code mediaUri} and returns the {@link Uri} for the downloaded file
* <p>
* If the {@code mediaUri} is already in the the local store, no download will be done and the given
* {@code mediaUri} will be returned instead. This may return null if the download fails.
* <p>
* The current thread is blocked until the download is finished.
*
* @return A local {@link Uri} or null if the download failed
*/
public static @Nullable Uri fetchMedia(@NonNull Context context, @NonNull Uri mediaUri) {
if (MediaUtils.isInMediaStore(mediaUri)) {
return mediaUri;
}

try {
// Do not download the file in async task. See
// https://github.com/wordpress-mobile/WordPress-Android/issues/5818
Uri downloadedUri = null;
try {
downloadedUri = MediaUtils.downloadExternalMedia(context, mediaUri);
} catch (IllegalStateException e) {
// Ref: https://github.com/wordpress-mobile/WordPress-Android/issues/5823
AppLog.e(AppLog.T.UTILS, "Can't download the image at: " + mediaUri.toString(), e);
CrashlyticsUtils.logException(e, AppLog.T.MEDIA, "Can't download the image at: " + mediaUri.toString()
+ " See issue #5823");
}
if (downloadedUri != null) {
listener.doNext(downloadedUri);
} else {
ToastUtils.showToast(context, R.string.error_downloading_image,
ToastUtils.Duration.SHORT);
return false;
}
return MediaUtils.downloadExternalMedia(context, mediaUri);
} catch (IllegalStateException e) {
// Ref: https://github.com/wordpress-mobile/WordPress-Android/issues/5823
AppLog.e(AppLog.T.UTILS, "Can't download the image at: " + mediaUri.toString(), e);
CrashlyticsUtils.logException(e, AppLog.T.MEDIA, "Can't download the image at: " + mediaUri.toString()
+ " See issue #5823");

return null;
}
}

/**
* Downloads the given {@code mediaUri} and calls {@code listener} if successful
* <p>
* If the download fails, a {@link android.widget.Toast} will be shown.
*
* @return A {@link Boolean} indicating whether the download was successful
*/
public static boolean fetchMediaAndDoNext(Context context, Uri mediaUri, MediaFetchDoNext listener) {
final Uri downloadedUri = fetchMedia(context, mediaUri);
if (downloadedUri != null) {
listener.doNext(downloadedUri);
return true;
} else {
listener.doNext(mediaUri);
ToastUtils.showToast(context, R.string.error_downloading_image,
ToastUtils.Duration.SHORT);
return false;
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.wordpress.android.viewmodel.giphy

import android.arch.lifecycle.ViewModel
import kotlinx.coroutines.experimental.CoroutineScope
import kotlinx.coroutines.experimental.Dispatchers
import kotlinx.coroutines.experimental.Job
import kotlin.coroutines.experimental.CoroutineContext

/**
* A base class for implementing Android [ViewModel] classes that also act as a [CoroutineScope].
*
* One advantage of using this is that coroutine builders like [async] or [launch] will become children of the parent
* [coroutineJob] declared in this [ViewModel]. If this [ViewModel] is cleared from memory, all the active child
* coroutines will be automatically cancelled.
*
* This strategy is called _Structured Concurrency_. Learn more about it here:
*
* - [Structured concurrency, lifecycle and coroutine parent-child hierarchy](https://goo.gl/rH3rmt)
* - [KotlinConf 2018 - Kotlin Coroutines in Practice by Roman Elizarov](https://www.youtube.com/watch?v=a3agLJQ6vt8)
*/
abstract class CoroutineScopedViewModel : ViewModel(), CoroutineScope {
/**
* The default parent [CoroutineContext] of all coroutine builders under this [ViewModel] ([CoroutineScope]).
*/
protected val coroutineJob = Job()

/**
* Sets it up so that all coroutine builders like [async] and [launch] will become children of [coroutineJob]
* and use the background thread dispatcher defined by [Dispatchers.Default].
*/
override val coroutineContext: CoroutineContext get() = coroutineJob + Dispatchers.Default

/**
* Cancel all active child coroutines.
*/
override fun onCleared() {
super.onCleared()
coroutineJob.cancel()
}
}
Loading

0 comments on commit 316fb59

Please sign in to comment.