diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/media')
19 files changed, 5997 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaAddFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaAddFragment.java new file mode 100644 index 000000000..ea8d06f47 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaAddFragment.java @@ -0,0 +1,281 @@ +package org.wordpress.android.ui.media; + +import android.app.Activity; +import android.app.Fragment; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.graphics.BitmapFactory; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.MediaStore; +import android.support.v4.content.CursorLoader; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import org.wordpress.android.BuildConfig; +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.MediaUploadState; +import org.wordpress.android.ui.RequestCodes; +import org.wordpress.android.ui.media.WordPressMediaUtils.LaunchCameraCallback; +import org.wordpress.android.ui.media.services.MediaEvents.MediaChanged; +import org.wordpress.android.ui.media.services.MediaUploadService; +import org.wordpress.android.util.MediaUtils; +import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.helpers.MediaFile; + +import java.io.File; +import java.util.List; + +import de.greenrobot.event.EventBus; + +/** + * An invisible fragment in charge of launching the right intents to camera, video, and image library. + * Also queues up media for upload and listens to notifications from the upload service. + */ +public class MediaAddFragment extends Fragment implements LaunchCameraCallback { + private static final String BUNDLE_MEDIA_CAPTURE_PATH = "mediaCapturePath"; + private String mMediaCapturePath = ""; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // This view doesn't really matter as this fragment is invisible + + if (savedInstanceState != null && savedInstanceState.getString(BUNDLE_MEDIA_CAPTURE_PATH) != null) + mMediaCapturePath = savedInstanceState.getString(BUNDLE_MEDIA_CAPTURE_PATH); + + return inflater.inflate(R.layout.actionbar_add_media_cell, container, false); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mMediaCapturePath != null && !mMediaCapturePath.equals("")) + outState.putString(BUNDLE_MEDIA_CAPTURE_PATH, mMediaCapturePath); + } + + @Override + public void onStart() { + super.onStart(); + // register context for change in connection status + getActivity().registerReceiver(mReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + } + + @Override + public void onStop() { + getActivity().unregisterReceiver(mReceiver); + super.onStop(); + } + + @Override + public void onResume() { + super.onResume(); + + resumeMediaUploadService(); + } + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) { + // Coming from zero connection. Re-register upload intent. + resumeMediaUploadService(); + } + } + }; + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (data != null || requestCode == RequestCodes.TAKE_PHOTO || + requestCode == RequestCodes.TAKE_VIDEO) { + String path; + + switch (requestCode) { + case RequestCodes.PICTURE_LIBRARY: + case RequestCodes.VIDEO_LIBRARY: + Uri imageUri = data.getData(); + fetchMedia(imageUri); + break; + case RequestCodes.TAKE_PHOTO: + if (resultCode == Activity.RESULT_OK) { + path = mMediaCapturePath; + mMediaCapturePath = null; + queueFileForUpload(path); + } + break; + case RequestCodes.TAKE_VIDEO: + if (resultCode == Activity.RESULT_OK) { + path = getRealPathFromURI(MediaUtils.getLastRecordedVideoUri(getActivity())); + queueFileForUpload(path); + } + break; + } + } + } + + private void fetchMedia(Uri mediaUri) { + if (!MediaUtils.isInMediaStore(mediaUri)) { + // Create an AsyncTask to download the file + new DownloadMediaTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mediaUri); + } else { + // It is a regular local media file + String path = getRealPathFromURI(mediaUri); + queueFileForUpload(path); + } + } + + private String getRealPathFromURI(Uri uri) { + String path; + if ("content".equals(uri.getScheme())) { + path = getRealPathFromContentURI(uri); + } else if ("file".equals(uri.getScheme())) { + path = uri.getPath(); + } else { + path = uri.toString(); + } + return path; + } + + private String getRealPathFromContentURI(Uri contentUri) { + if (contentUri == null) + return null; + + String[] proj = { MediaStore.Images.Media.DATA }; + CursorLoader loader = new CursorLoader(getActivity(), contentUri, proj, null, null, null); + Cursor cursor = loader.loadInBackground(); + + if (cursor == null) + return null; + + int column_index = cursor.getColumnIndex(MediaStore.Images.Media.DATA); + if (column_index == -1) { + cursor.close(); + return null; + } + + String path; + if (cursor.moveToFirst()) { + path = cursor.getString(column_index); + } else { + path = null; + } + + cursor.close(); + return path; + } + + private void queueFileForUpload(String path) { + if (path == null || path.equals("")) { + Toast.makeText(getActivity(), "Error opening file", Toast.LENGTH_SHORT).show(); + return; + } + + Blog blog = WordPress.getCurrentBlog(); + + File file = new File(path); + if (!file.exists()) { + return; + } + + String mimeType = MediaUtils.getMediaFileMimeType(file); + String fileName = MediaUtils.getMediaFileName(file, mimeType); + + MediaFile mediaFile = new MediaFile(); + mediaFile.setBlogId(String.valueOf(blog.getLocalTableBlogId())); + mediaFile.setFileName(fileName); + mediaFile.setFilePath(path); + mediaFile.setUploadState("queued"); + mediaFile.setDateCreatedGMT(System.currentTimeMillis()); + mediaFile.setMediaId(String.valueOf(System.currentTimeMillis())); + if (mimeType != null && mimeType.startsWith("image")) { + // get width and height + BitmapFactory.Options bfo = new BitmapFactory.Options(); + bfo.inJustDecodeBounds = true; + BitmapFactory.decodeFile(path, bfo); + mediaFile.setWidth(bfo.outWidth); + mediaFile.setHeight(bfo.outHeight); + } + + if (!TextUtils.isEmpty(mimeType)) { + mediaFile.setMimeType(mimeType); + } + WordPress.wpDB.saveMediaFile(mediaFile); + EventBus.getDefault().post(new MediaChanged(String.valueOf(blog.getLocalTableBlogId()), mediaFile.getMediaId())); + startMediaUploadService(); + } + + private void startMediaUploadService() { + if (NetworkUtils.isNetworkAvailable(getActivity())) { + getActivity().startService(new Intent(getActivity(), MediaUploadService.class)); + } + } + + private void resumeMediaUploadService() { + startMediaUploadService(); + } + + @Override + public void onMediaCapturePathReady(String mediaCapturePath) { + mMediaCapturePath = mediaCapturePath; + } + + public void launchCamera() { + WordPressMediaUtils.launchCamera(this, BuildConfig.APPLICATION_ID, this); + } + + public void launchVideoCamera() { + WordPressMediaUtils.launchVideoCamera(this); + } + + public void launchVideoLibrary() { + WordPressMediaUtils.launchVideoLibrary(this); + } + + public void launchPictureLibrary() { + WordPressMediaUtils.launchPictureLibrary(this); + } + + public void addToQueue(String mediaId) { + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + WordPress.wpDB.updateMediaUploadState(blogId, mediaId, MediaUploadState.QUEUED); + startMediaUploadService(); + } + + public void uploadList(List<Uri> uriList) { + for (Uri uri : uriList) { + fetchMedia(uri); + } + } + + private class DownloadMediaTask extends AsyncTask<Uri, Integer, Uri> { + @Override + protected Uri doInBackground(Uri... uris) { + Uri imageUri = uris[0]; + return MediaUtils.downloadExternalMedia(getActivity(), imageUri); + } + + protected void onPostExecute(Uri newUri) { + if (getActivity() == null) + return; + + if (newUri != null) { + String path = getRealPathFromURI(newUri); + queueFileForUpload(path); + } + else + Toast.makeText(getActivity(), getString(R.string.error_downloading_image), Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java new file mode 100644 index 000000000..2e85481fb --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java @@ -0,0 +1,583 @@ +package org.wordpress.android.ui.media; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.graphics.drawable.ColorDrawable; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.view.MenuItemCompat; +import android.support.v4.view.MenuItemCompat.OnActionExpandListener; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.SearchView; +import android.support.v7.widget.SearchView.OnQueryTextListener; +import android.support.v7.widget.Toolbar; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.PopupWindow; +import android.widget.Toast; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.FeatureSet; +import org.wordpress.android.ui.ActivityId; +import org.wordpress.android.ui.media.MediaEditFragment.MediaEditFragmentCallback; +import org.wordpress.android.ui.media.MediaGridFragment.Filter; +import org.wordpress.android.ui.media.MediaGridFragment.MediaGridListener; +import org.wordpress.android.ui.media.MediaItemFragment.MediaItemFragmentCallback; +import org.wordpress.android.ui.media.services.MediaDeleteService; +import org.wordpress.android.ui.media.services.MediaEvents; +import org.wordpress.android.util.ActivityUtils; +import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.PermissionUtils; +import org.wordpress.android.util.ToastUtils; +import org.xmlrpc.android.ApiHelper; +import org.xmlrpc.android.ApiHelper.GetFeatures.Callback; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import de.greenrobot.event.EventBus; + +/** + * The main activity in which the user can browse their media. + */ +public class MediaBrowserActivity extends AppCompatActivity implements MediaGridListener, + MediaItemFragmentCallback, OnQueryTextListener, OnActionExpandListener, + MediaEditFragmentCallback { + private static final String SAVED_QUERY = "SAVED_QUERY"; + public static final int MEDIA_PERMISSION_REQUEST_CODE = 1; + + private MediaGridFragment mMediaGridFragment; + private MediaItemFragment mMediaItemFragment; + private MediaEditFragment mMediaEditFragment; + private MediaAddFragment mMediaAddFragment; + private PopupWindow mAddMediaPopup; + + private SearchView mSearchView; + private MenuItem mSearchMenuItem; + private Menu mMenu; + private FeatureSet mFeatureSet; + private String mQuery; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) { + // Coming from zero connection. Continue what's pending for delete + int blogId = WordPress.getCurrentLocalTableBlogId(); + if (blogId != -1 && WordPress.wpDB.hasMediaDeleteQueueItems(blogId)) { + startMediaDeleteService(); + } + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // This should be removed when #2734 is fixed + if (WordPress.getCurrentBlog() == null) { + ToastUtils.showToast(this, R.string.blog_not_found, ToastUtils.Duration.SHORT); + finish(); + return; + } + + setContentView(R.layout.media_browser_activity); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayShowTitleEnabled(true); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setTitle(R.string.media); + + FragmentManager fm = getFragmentManager(); + fm.addOnBackStackChangedListener(mOnBackStackChangedListener); + FragmentTransaction ft = fm.beginTransaction(); + + mMediaAddFragment = (MediaAddFragment) fm.findFragmentById(R.id.mediaAddFragment); + mMediaGridFragment = (MediaGridFragment) fm.findFragmentById(R.id.mediaGridFragment); + + mMediaItemFragment = (MediaItemFragment) fm.findFragmentByTag(MediaItemFragment.TAG); + if (mMediaItemFragment != null) + ft.hide(mMediaGridFragment); + + mMediaEditFragment = (MediaEditFragment) fm.findFragmentByTag(MediaEditFragment.TAG); + if (mMediaEditFragment != null && !mMediaEditFragment.isInLayout()) + ft.hide(mMediaItemFragment); + + ft.commitAllowingStateLoss(); + + setupAddMenuPopup(); + + String action = getIntent().getAction(); + if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { + // We arrived here from a share action + uploadSharedFiles(); + } + } + + @Override + public void onStart() { + super.onStart(); + registerReceiver(mReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + EventBus.getDefault().unregister(this); + unregisterReceiver(mReceiver); + super.onStop(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(SAVED_QUERY, mQuery); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mQuery = savedInstanceState.getString(SAVED_QUERY); + } + + private void uploadSharedFiles() { + Intent intent = getIntent(); + String action = intent.getAction(); + final List<Uri> multi_stream; + if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { + multi_stream = intent.getParcelableArrayListExtra((Intent.EXTRA_STREAM)); + } else { + multi_stream = new ArrayList<>(); + multi_stream.add((Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM)); + } + mMediaAddFragment.uploadList(multi_stream); + + // clear the intent's action, so that in case the user rotates, we don't re-upload the same + // files + getIntent().setAction(null); + } + + private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener = new FragmentManager.OnBackStackChangedListener() { + public void onBackStackChanged() { + FragmentManager manager = getFragmentManager(); + MediaGridFragment mediaGridFragment = (MediaGridFragment)manager.findFragmentById(R.id.mediaGridFragment); + if (mediaGridFragment.isVisible()) { + mediaGridFragment.refreshSpinnerAdapter(); + } + ActivityUtils.hideKeyboard(MediaBrowserActivity.this); + } + }; + + /** Setup the popup that allows you to add new media from camera, video camera or local files **/ + private void setupAddMenuPopup() { + String capturePhoto = getResources().getString(R.string.media_add_popup_capture_photo); + String captureVideo = getResources().getString(R.string.media_add_popup_capture_video); + String pickPhotoFromGallery = getResources().getString(R.string.select_photo); + String pickVideoFromGallery = getResources().getString(R.string.select_video); + final ArrayAdapter<String> adapter = new ArrayAdapter<>(MediaBrowserActivity.this, + R.layout.actionbar_add_media_cell, + new String[] { + capturePhoto, captureVideo, pickPhotoFromGallery, pickVideoFromGallery + }); + + View layoutView = getLayoutInflater().inflate(R.layout.actionbar_add_media, null, false); + ListView listView = (ListView) layoutView.findViewById(R.id.actionbar_add_media_listview); + listView.setAdapter(adapter); + listView.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + adapter.notifyDataSetChanged(); + + if (position == 0) { + mMediaAddFragment.launchCamera(); + } else if (position == 1) { + mMediaAddFragment.launchVideoCamera(); + } else if (position == 2) { + mMediaAddFragment.launchPictureLibrary(); + } else if (position == 3) { + mMediaAddFragment.launchVideoLibrary(); + } + + mAddMediaPopup.dismiss(); + } + }); + + int width = getResources().getDimensionPixelSize(R.dimen.action_bar_spinner_width); + + mAddMediaPopup = new PopupWindow(layoutView, width, ViewGroup.LayoutParams.WRAP_CONTENT, true); + mAddMediaPopup.setBackgroundDrawable(new ColorDrawable()); + } + + @Override + protected void onResume() { + super.onResume(); + startMediaDeleteService(); + getFeatureSet(); + ActivityId.trackLastActivity(ActivityId.MEDIA); + } + + /** Get the feature set for a wordpress.com hosted blog **/ + private void getFeatureSet() { + if (WordPress.getCurrentBlog() == null || !WordPress.getCurrentBlog().isDotcomFlag()) + return; + + ApiHelper.GetFeatures task = new ApiHelper.GetFeatures(new Callback() { + @Override + public void onResult(FeatureSet featureSet) { + mFeatureSet = featureSet; + } + + }); + + List<Object> apiArgs = new ArrayList<>(); + apiArgs.add(WordPress.getCurrentBlog()); + task.execute(apiArgs); + } + + @Override + protected void onPause() { + super.onPause(); + + if (mSearchMenuItem != null) { + String tempQuery = mQuery; + MenuItemCompat.collapseActionView(mSearchMenuItem); + mQuery = tempQuery; + } + } + + @Override + public void onMediaItemSelected(String mediaId) { + String tempQuery = mQuery; + if (mSearchView != null) { + mSearchView.clearFocus(); + } + + if (mSearchMenuItem != null) { + MenuItemCompat.collapseActionView(mSearchMenuItem); + } + + FragmentManager fm = getFragmentManager(); + if (fm.getBackStackEntryCount() == 0) { + FragmentTransaction ft = fm.beginTransaction(); + ft.hide(mMediaGridFragment); + mMediaGridFragment.clearSelectedItems(); + mMediaItemFragment = MediaItemFragment.newInstance(mediaId); + ft.add(R.id.media_browser_container, mMediaItemFragment, MediaItemFragment.TAG); + ft.addToBackStack(null); + ft.commitAllowingStateLoss(); + mQuery = tempQuery; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + mMenu = menu; + getMenuInflater().inflate(R.menu.media, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + mSearchView = (SearchView) menu.findItem(R.id.menu_search).getActionView(); + mSearchView.setOnQueryTextListener(this); + + mSearchMenuItem = menu.findItem(R.id.menu_search); + MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, this); + + //open search bar if we were searching for something before + if (!TextUtils.isEmpty(mQuery) && mMediaGridFragment != null && mMediaGridFragment.isVisible()) { + String tempQuery = mQuery; //temporary hold onto query + MenuItemCompat.expandActionView(mSearchMenuItem); //this will reset mQuery + onQueryTextSubmit(tempQuery); + mSearchView.setQuery(mQuery, true); + } + + return super.onPrepareOptionsMenu(menu); + + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], + @NonNull int[] grantResults) { + switch (requestCode) { + case MEDIA_PERMISSION_REQUEST_CODE: + for (int grantResult : grantResults) { + if (grantResult == PackageManager.PERMISSION_DENIED) { + ToastUtils.showToast(this, getString(R.string.add_media_permission_required)); + return; + } + } + showNewMediaMenu(); + break; + default: + break; + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int i = item.getItemId(); + if (i == android.R.id.home) { + onBackPressed(); + return true; + } else if (i == R.id.menu_new_media) { + if (PermissionUtils.checkAndRequestCameraAndStoragePermissions(this, MEDIA_PERMISSION_REQUEST_CODE)) { + showNewMediaMenu(); + } + return true; + } else if (i == R.id.menu_search) { + mSearchMenuItem = item; + MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, this); + MenuItemCompat.expandActionView(mSearchMenuItem); + + mSearchView = (SearchView) item.getActionView(); + mSearchView.setOnQueryTextListener(this); + + // load last saved query + if (!TextUtils.isEmpty(mQuery)) { + onQueryTextSubmit(mQuery); + mSearchView.setQuery(mQuery, true); + } + return true; + } else if (i == R.id.menu_edit_media) { + String mediaId = mMediaItemFragment.getMediaId(); + FragmentManager fm = getFragmentManager(); + + if (mMediaEditFragment == null || !mMediaEditFragment.isInLayout()) { + // phone layout: hide item details, show and update edit fragment + FragmentTransaction ft = fm.beginTransaction(); + + if (mMediaItemFragment.isVisible()) + ft.hide(mMediaItemFragment); + + mMediaEditFragment = MediaEditFragment.newInstance(mediaId); + ft.add(R.id.media_browser_container, mMediaEditFragment, MediaEditFragment.TAG); + ft.addToBackStack(null); + ft.commitAllowingStateLoss(); + } else { + // tablet layout: update edit fragment + mMediaEditFragment.loadMedia(mediaId); + } + + if (mSearchView != null) { + mSearchView.clearFocus(); + } + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void onMediaItemListDownloaded() { + if (mMediaItemFragment != null) { + mMediaGridFragment.setRefreshing(false); + if (mMediaItemFragment.isInLayout()) { + mMediaItemFragment.loadDefaultMedia(); + } + } + } + + @Override + public void onMediaItemListDownloadStart() { + mMediaGridFragment.setRefreshing(true); + } + + @Override + public boolean onQueryTextSubmit(String query) { + if (mMediaGridFragment != null) { + mMediaGridFragment.search(query); + } + mQuery = query; + mSearchView.clearFocus(); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + if (mMediaGridFragment != null) { + mMediaGridFragment.search(newText); + } + mQuery = newText; + return true; + } + + @Override + public void onResume(Fragment fragment) { + invalidateOptionsMenu(); + } + + @Override + public void onPause(Fragment fragment) { + invalidateOptionsMenu(); + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + // currently we don't support searching from within a filter, so hide it + if (mMediaGridFragment != null) { + mMediaGridFragment.setFilterVisibility(View.GONE); + mMediaGridFragment.setFilter(Filter.ALL); + } + + // load last search query + if (!TextUtils.isEmpty(mQuery)) + onQueryTextChange(mQuery); + mMenu.findItem(R.id.menu_new_media).setVisible(false); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + if (mMediaGridFragment != null) { + mMediaGridFragment.setFilterVisibility(View.VISIBLE); + mMediaGridFragment.setFilter(Filter.ALL); + } + mMenu.findItem(R.id.menu_new_media).setVisible(true); + return true; + } + + public void onSavedEdit(String mediaId, boolean result) { + if (mMediaEditFragment != null && mMediaEditFragment.isVisible() && result) { + FragmentManager fm = getFragmentManager(); + fm.popBackStack(); + + // refresh media item details (phone-only) + if (mMediaItemFragment != null) + mMediaItemFragment.loadMedia(mediaId); + + // refresh grid + mMediaGridFragment.refreshMediaFromDB(); + } + } + + private void startMediaDeleteService() { + if (NetworkUtils.isNetworkAvailable(this)) { + startService(new Intent(this, MediaDeleteService.class)); + } + } + + @Override + public void onBackPressed() { + FragmentManager fm = getFragmentManager(); + if (fm.getBackStackEntryCount() > 0) { + fm.popBackStack(); + } else { + super.onBackPressed(); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(MediaEvents.MediaChanged event) { + updateOnMediaChanged(event.mLocalBlogId, event.mMediaId); + } + + @SuppressWarnings("unused") + public void onEventMainThread(MediaEvents.MediaUploadSucceeded event) { + updateOnMediaChanged(event.mLocalBlogId, event.mLocalMediaId); + } + + @SuppressWarnings("unused") + public void onEventMainThread(MediaEvents.MediaUploadFailed event) { + ToastUtils.showToast(this, event.mErrorMessage, ToastUtils.Duration.LONG); + } + + public void updateOnMediaChanged(String blogId, String mediaId) { + if (mediaId == null) { + return; + } + + // If the media was deleted, remove it from multi select (if it was selected) and hide it from the the detail + // view (if it was the one displayed) + if (!WordPress.wpDB.mediaFileExists(blogId, mediaId)) { + mMediaGridFragment.removeFromMultiSelect(mediaId); + if (mMediaEditFragment != null && mMediaEditFragment.isVisible() + && mediaId.equals(mMediaEditFragment.getMediaId())) { + if (mMediaEditFragment.isInLayout()) { + mMediaEditFragment.loadMedia(null); + } else { + getFragmentManager().popBackStack(); + } + } + } + + // Update Grid view + mMediaGridFragment.refreshMediaFromDB(); + + // Update Spinner views + mMediaGridFragment.updateFilterText(); + mMediaGridFragment.updateSpinnerAdapter(); + } + + @Override + public void onRetryUpload(String mediaId) { + mMediaAddFragment.addToQueue(mediaId); + } + + public void deleteMedia(final ArrayList<String> ids) { + final String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + Set<String> sanitizedIds = new HashSet<>(ids.size()); + + // phone layout: pop the item fragment if it's visible + getFragmentManager().popBackStack(); + + // Make sure there are no media in "uploading" + for (String currentID : ids) { + if (WordPressMediaUtils.canDeleteMedia(blogId, currentID)) { + sanitizedIds.add(currentID); + } + } + + if (sanitizedIds.size() != ids.size()) { + if (ids.size() == 1) { + Toast.makeText(this, R.string.wait_until_upload_completes, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(this, R.string.cannot_delete_multi_media_items, Toast.LENGTH_LONG).show(); + } + } + + // mark items for delete without actually deleting items yet, + // and then refresh the grid + WordPress.wpDB.setMediaFilesMarkedForDelete(blogId, sanitizedIds); + startMediaDeleteService(); + if (mMediaGridFragment != null) { + mMediaGridFragment.clearSelectedItems(); + mMediaGridFragment.refreshMediaFromDB(); + } + } + + private void showNewMediaMenu() { + View view = findViewById(R.id.menu_new_media); + if (view != null) { + int y_offset = getResources().getDimensionPixelSize(R.dimen.action_bar_spinner_y_offset); + int[] loc = new int[2]; + view.getLocationOnScreen(loc); + mAddMediaPopup.showAtLocation(view, Gravity.TOP | Gravity.LEFT, loc[0], + loc[1] + view.getHeight() + y_offset); + } else { + // In case menu button is not on screen (declared showAsAction="ifRoom"), center the popup in the view. + View gridView = findViewById(R.id.media_gridview); + mAddMediaPopup.showAtLocation(gridView, Gravity.CENTER, 0, 0); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaEditFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaEditFragment.java new file mode 100644 index 000000000..6b9dbb0e8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaEditFragment.java @@ -0,0 +1,385 @@ +package org.wordpress.android.ui.media; + +import android.app.Activity; +import android.app.Fragment; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ScrollView; +import android.widget.Toast; + +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.NetworkImageView; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.WordPressDB; +import org.wordpress.android.models.Blog; +import org.wordpress.android.util.ActivityUtils; +import org.wordpress.android.util.ImageUtils.BitmapWorkerCallback; +import org.wordpress.android.util.ImageUtils.BitmapWorkerTask; +import org.wordpress.android.util.MediaUtils; +import org.xmlrpc.android.ApiHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * A fragment for editing media on the Media tab + */ +public class MediaEditFragment extends Fragment { + private static final String ARGS_MEDIA_ID = "media_id"; + // also appears in the layouts, from the strings.xml + public static final String TAG = "MediaEditFragment"; + + private NetworkImageView mNetworkImageView; + private ImageView mLocalImageView; + private EditText mTitleView; + private EditText mCaptionView; + private EditText mDescriptionView; + private Button mSaveButton; + + private MediaEditFragmentCallback mCallback; + + private boolean mIsMediaUpdating = false; + + private String mMediaId; + private ScrollView mScrollView; + private View mLinearLayout; + private ImageLoader mImageLoader; + + public interface MediaEditFragmentCallback { + void onResume(Fragment fragment); + void onPause(Fragment fragment); + void onSavedEdit(String mediaId, boolean result); + } + + public static MediaEditFragment newInstance(String mediaId) { + MediaEditFragment fragment = new MediaEditFragment(); + + Bundle args = new Bundle(); + args.putString(ARGS_MEDIA_ID, mediaId); + fragment.setArguments(args); + + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + mImageLoader = MediaImageLoader.getInstance(); + + // retain this fragment across configuration changes + setRetainInstance(true); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mCallback = (MediaEditFragmentCallback) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must implement " + + MediaEditFragmentCallback.class.getSimpleName()); + } + } + + + @Override + public void onDetach() { + super.onDetach(); + // set callback to null so we don't accidentally leak the activity instance + mCallback = null; + } + + private boolean hasCallback() { + return (mCallback != null); + } + + @Override + public void onResume() { + super.onResume(); + if (hasCallback()) { + mCallback.onResume(this); + } + } + + @Override + public void onPause() { + super.onPause(); + if (hasCallback()) { + mCallback.onPause(this); + } + } + + public String getMediaId() { + if (mMediaId != null) { + return mMediaId; + } else if (getArguments() != null) { + mMediaId = getArguments().getString(ARGS_MEDIA_ID); + return mMediaId; + } else { + return null; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mScrollView = (ScrollView) inflater.inflate(R.layout.media_edit_fragment, container, false); + + mLinearLayout = mScrollView.findViewById(R.id.media_edit_linear_layout); + mTitleView = (EditText) mScrollView.findViewById(R.id.media_edit_fragment_title); + mCaptionView = (EditText) mScrollView.findViewById(R.id.media_edit_fragment_caption); + mDescriptionView = (EditText) mScrollView.findViewById(R.id.media_edit_fragment_description); + mLocalImageView = (ImageView) mScrollView.findViewById(R.id.media_edit_fragment_image_local); + mNetworkImageView = (NetworkImageView) mScrollView.findViewById(R.id.media_edit_fragment_image_network); + mSaveButton = (Button) mScrollView.findViewById(R.id.media_edit_save_button); + mSaveButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + editMedia(); + } + }); + + disableEditingOnOldVersion(); + + loadMedia(getMediaId()); + + return mScrollView; + } + + private void disableEditingOnOldVersion() { + if (WordPressMediaUtils.isWordPressVersionWithMediaEditingCapabilities()) { + return; + } + + mSaveButton.setEnabled(false); + mTitleView.setEnabled(false); + mCaptionView.setEnabled(false); + mDescriptionView.setEnabled(false); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + } + + public void loadMedia(String mediaId) { + mMediaId = mediaId; + Blog blog = WordPress.getCurrentBlog(); + + if (blog != null && getActivity() != null) { + String blogId = String.valueOf(blog.getLocalTableBlogId()); + + if (mMediaId != null) { + Cursor cursor = WordPress.wpDB.getMediaFile(blogId, mMediaId); + refreshViews(cursor); + cursor.close(); + } else { + refreshViews(null); + } + } + } + + void editMedia() { + ActivityUtils.hideKeyboard(getActivity()); + final String mediaId = this.getMediaId(); + final String title = mTitleView.getText().toString(); + final String description = mDescriptionView.getText().toString(); + final Blog currentBlog = WordPress.getCurrentBlog(); + final String caption = mCaptionView.getText().toString(); + + ApiHelper.EditMediaItemTask task = new ApiHelper.EditMediaItemTask(mediaId, title, description, caption, + new ApiHelper.GenericCallback() { + @Override + public void onSuccess() { + String blogId = String.valueOf(currentBlog.getLocalTableBlogId()); + WordPress.wpDB.updateMediaFile(blogId, mediaId, title, description, caption); + if (getActivity() != null) { + Toast.makeText(getActivity(), R.string.media_edit_success, Toast.LENGTH_LONG).show(); + } + setMediaUpdating(false); + if (hasCallback()) { + mCallback.onSavedEdit(mediaId, true); + } + } + + @Override + public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) { + if (getActivity() != null) { + Toast.makeText(getActivity(), R.string.media_edit_failure, Toast.LENGTH_LONG).show(); + getActivity().invalidateOptionsMenu(); + } + setMediaUpdating(false); + if (hasCallback()) { + mCallback.onSavedEdit(mediaId, false); + } + } + } + ); + + List<Object> apiArgs = new ArrayList<Object>(); + apiArgs.add(currentBlog); + + if (!isMediaUpdating()) { + setMediaUpdating(true); + task.execute(apiArgs); + } + } + + private void setMediaUpdating(boolean isUpdating) { + mIsMediaUpdating = isUpdating; + mSaveButton.setEnabled(!isUpdating); + + if (isUpdating) { + mSaveButton.setText(R.string.saving); + } else { + mSaveButton.setText(R.string.save); + } + } + + private boolean isMediaUpdating() { + return mIsMediaUpdating; + } + + private void refreshImageView(Cursor cursor, boolean isLocal) { + final String imageUri; + if (isLocal) { + imageUri = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH)); + } else { + imageUri = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL)); + } + if (MediaUtils.isValidImage(imageUri)) { + int width = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_WIDTH)); + int height = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_HEIGHT)); + + // differentiating between tablet and phone + float screenWidth; + if (this.isInLayout()) { + screenWidth = mLinearLayout.getMeasuredWidth(); + } else { + screenWidth = getActivity().getResources().getDisplayMetrics().widthPixels; + } + float screenHeight = getActivity().getResources().getDisplayMetrics().heightPixels; + + if (width > screenWidth) { + height = (int) (height / (width / screenWidth)); + } else if (height > screenHeight) { + width = (int) (width / (height / screenHeight)); + } + + if (isLocal) { + loadLocalImage(mLocalImageView, imageUri, width, height); + mLocalImageView.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, height)); + } else { + mNetworkImageView.setImageUrl(imageUri + "?w=" + screenWidth, mImageLoader); + mNetworkImageView.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, height)); + } + } else { + mNetworkImageView.setVisibility(View.GONE); + mLocalImageView.setVisibility(View.GONE); + } + } + + private void refreshViews(Cursor cursor) { + if (cursor == null || !cursor.moveToFirst() || cursor.getCount() == 0) { + mLinearLayout.setVisibility(View.GONE); + return; + } + + mLinearLayout.setVisibility(View.VISIBLE); + + mScrollView.scrollTo(0, 0); + + String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE)); + boolean isLocal = MediaUtils.isLocalFile(state); + if (isLocal) { + mNetworkImageView.setVisibility(View.GONE); + mLocalImageView.setVisibility(View.VISIBLE); + } else { + mNetworkImageView.setVisibility(View.VISIBLE); + mLocalImageView.setVisibility(View.GONE); + } + + // user can't edit local files + mSaveButton.setEnabled(!isLocal); + mTitleView.setEnabled(!isLocal); + mCaptionView.setEnabled(!isLocal); + mDescriptionView.setEnabled(!isLocal); + + mMediaId = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID)); + mTitleView.setText(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_TITLE))); + mTitleView.requestFocus(); + mTitleView.setSelection(mTitleView.getText().length()); + mCaptionView.setText(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_CAPTION))); + mDescriptionView.setText(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DESCRIPTION))); + + refreshImageView(cursor, isLocal); + disableEditingOnOldVersion(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (!isInLayout()) { + inflater.inflate(R.menu.media_edit, menu); + } + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + if (!isInLayout()) { + menu.findItem(R.id.menu_new_media).setVisible(false); + menu.findItem(R.id.menu_search).setVisible(false); + + if (!WordPressMediaUtils.isWordPressVersionWithMediaEditingCapabilities()) { + menu.findItem(R.id.menu_save_media).setVisible(false); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + if (itemId == R.id.menu_save_media) { + item.setActionView(R.layout.progressbar); + editMedia(); + } + return super.onOptionsItemSelected(item); + } + + private synchronized void loadLocalImage(ImageView imageView, String filePath, int width, int height) { + if (MediaUtils.isValidImage(filePath)) { + imageView.setTag(filePath); + + Bitmap bitmap = WordPress.getBitmapCache().get(filePath); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } else { + BitmapWorkerTask task = new BitmapWorkerTask(imageView, width, height, new BitmapWorkerCallback() { + @Override + public void onBitmapReady(String path, ImageView imageView, Bitmap bitmap) { + if (imageView != null) { + imageView.setImageBitmap(bitmap); + } + WordPress.getBitmapCache().put(path, bitmap); + } + }); + task.execute(filePath); + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryActivity.java new file mode 100644 index 000000000..e3168ed45 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryActivity.java @@ -0,0 +1,186 @@ +package org.wordpress.android.ui.media; + +import android.app.FragmentManager; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import com.sothree.slidinguppanel.SlidingUpPanelLayout; +import com.sothree.slidinguppanel.SlidingUpPanelLayout.PanelSlideListener; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.util.helpers.MediaGallery; +import org.wordpress.android.ui.media.MediaGallerySettingsFragment.MediaGallerySettingsCallback; +import org.wordpress.android.util.DisplayUtils; + +import java.util.ArrayList; + +/** + * An activity where the user can manage a media gallery + */ +public class MediaGalleryActivity extends AppCompatActivity implements MediaGallerySettingsCallback { + public static final int REQUEST_CODE = 3000; + + // params for the gallery + public static final String PARAMS_MEDIA_GALLERY = "PARAMS_MEDIA_GALLERY"; + + // launches media picker in onCreate() if set + public static final String PARAMS_LAUNCH_PICKER = "PARAMS_LAUNCH_PICKER"; + + // result of the gallery + public static final String RESULT_MEDIA_GALLERY = "RESULT_MEDIA_GALLERY"; + + private MediaGalleryEditFragment mMediaGalleryEditFragment; + private MediaGallerySettingsFragment mMediaGallerySettingsFragment; + + private SlidingUpPanelLayout mSlidingPanelLayout; + private boolean mIsPanelCollapsed = true; + + private MediaGallery mMediaGallery; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (WordPress.wpDB == null) { + Toast.makeText(this, R.string.fatal_db_error, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + setTitle(R.string.media_gallery_edit); + + setContentView(R.layout.media_gallery_activity); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(true); + } + + FragmentManager fm = getFragmentManager(); + + mMediaGallery = (MediaGallery) getIntent().getSerializableExtra(PARAMS_MEDIA_GALLERY); + if (mMediaGallery == null) { + mMediaGallery = new MediaGallery(); + } + + mMediaGalleryEditFragment = (MediaGalleryEditFragment) fm.findFragmentById(R.id.mediaGalleryEditFragment); + mMediaGallerySettingsFragment = (MediaGallerySettingsFragment) fm.findFragmentById( + R.id.mediaGallerySettingsFragment); + if (savedInstanceState == null) { + // if not null, the fragments will remember its state + mMediaGallerySettingsFragment.setRandom(mMediaGallery.isRandom()); + mMediaGallerySettingsFragment.setNumColumns(mMediaGallery.getNumColumns()); + mMediaGallerySettingsFragment.setType(mMediaGallery.getType()); + mMediaGalleryEditFragment.setMediaIds(mMediaGallery.getIds()); + } + + mSlidingPanelLayout = (SlidingUpPanelLayout) findViewById(R.id.media_gallery_root); + if (mSlidingPanelLayout != null) { + // sliding panel layout is on phone only + + mSlidingPanelLayout.setDragView(mMediaGallerySettingsFragment.getDragView()); + mSlidingPanelLayout.setPanelHeight(DisplayUtils.dpToPx(this, 48)); + mSlidingPanelLayout.setPanelSlideListener(new PanelSlideListener() { + @Override + public void onPanelSlide(View panel, float slideOffset) { + } + + @Override + public void onPanelExpanded(View panel) { + mMediaGallerySettingsFragment.onPanelExpanded(); + mIsPanelCollapsed = false; + } + + @Override + public void onPanelCollapsed(View panel) { + mMediaGallerySettingsFragment.onPanelCollapsed(); + mIsPanelCollapsed = true; + } + }); + } + + if (getIntent().hasExtra(PARAMS_LAUNCH_PICKER)) { + handleAddMedia(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.media_gallery, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.menu_add_media) { + handleAddMedia(); + return true; + } else if (item.getItemId() == R.id.menu_save) { + handleSaveMedia(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == MediaGalleryPickerActivity.REQUEST_CODE) { + if (resultCode == RESULT_OK) { + ArrayList<String> ids = data.getStringArrayListExtra(MediaGalleryPickerActivity.RESULT_IDS); + mMediaGalleryEditFragment.setMediaIds(ids); + } + } + super.onActivityResult(requestCode, resultCode, data); + } + + private void handleAddMedia() { + // need to make MediaGalleryAdd into an activity rather than a fragment because I can't add this fragment + // on top of the slidingpanel layout (since it needs to be the root layout) + + ArrayList<String> mediaIds = mMediaGalleryEditFragment.getMediaIds(); + + Intent intent = new Intent(this, MediaGalleryPickerActivity.class); + intent.putExtra(MediaGalleryPickerActivity.PARAM_SELECTED_IDS, mediaIds); + startActivityForResult(intent, MediaGalleryPickerActivity.REQUEST_CODE); + } + + private void handleSaveMedia() { + Intent intent = new Intent(); + ArrayList<String> ids = mMediaGalleryEditFragment.getMediaIds(); + boolean isRandom = mMediaGallerySettingsFragment.isRandom(); + int numColumns = mMediaGallerySettingsFragment.getNumColumns(); + String type = mMediaGallerySettingsFragment.getType(); + + mMediaGallery.setIds(ids); + mMediaGallery.setRandom(isRandom); + mMediaGallery.setNumColumns(numColumns); + mMediaGallery.setType(type); + + intent.putExtra(RESULT_MEDIA_GALLERY, mMediaGallery); + setResult(RESULT_OK, intent); + finish(); + } + + @Override + public void onBackPressed() { + if (mSlidingPanelLayout != null && !mIsPanelCollapsed) { + mSlidingPanelLayout.collapsePane(); + } else { + super.onBackPressed(); + } + } + + @Override + public void onReverseClicked() { + mMediaGalleryEditFragment.reverseIds(); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryAdapter.java new file mode 100644 index 000000000..93a24ab2f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryAdapter.java @@ -0,0 +1,140 @@ +package org.wordpress.android.ui.media; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.NetworkImageView; +import com.mobeta.android.dslv.ResourceDragSortCursorAdapter; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.WordPressDB; +import org.wordpress.android.util.MediaUtils; +import org.wordpress.android.util.StringUtils; + +/** + * Adapter for a drag-sort listview where the user can drag media items to sort their order + * for a media gallery + */ +class MediaGalleryAdapter extends ResourceDragSortCursorAdapter { + private ImageLoader mImageLoader; + + public MediaGalleryAdapter(Context context, int layout, Cursor c, boolean autoRequery, ImageLoader imageLoader) { + super(context, layout, c, autoRequery); + setImageLoader(imageLoader); + } + + void setImageLoader(ImageLoader imageLoader) { + if (imageLoader != null) { + mImageLoader = imageLoader; + } else { + mImageLoader = WordPress.imageLoader; + } + } + + private static class GridViewHolder { + private final TextView filenameView; + private final TextView titleView; + private final TextView uploadDateView; + private final ImageView imageView; + private final TextView fileTypeView; + private final TextView dimensionView; + + GridViewHolder(View view) { + filenameView = (TextView) view.findViewById(R.id.media_grid_item_filename); + titleView = (TextView) view.findViewById(R.id.media_grid_item_name); + uploadDateView = (TextView) view.findViewById(R.id.media_grid_item_upload_date); + imageView = (ImageView) view.findViewById(R.id.media_grid_item_image); + fileTypeView = (TextView) view.findViewById(R.id.media_grid_item_filetype); + dimensionView = (TextView) view.findViewById(R.id.media_grid_item_dimension); + } + } + @Override + public void bindView(View view, Context context, Cursor cursor) { + final GridViewHolder holder; + if (view.getTag() instanceof GridViewHolder) { + holder = (GridViewHolder) view.getTag(); + } else { + holder = new GridViewHolder(view); + view.setTag(holder); + } + + String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE)); + boolean isLocalFile = MediaUtils.isLocalFile(state); + + // file name + String fileName = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME)); + if (holder.filenameView != null) { + holder.filenameView.setText(String.format(context.getString(R.string.media_file_name), fileName)); + } + + // title of media + String title = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_TITLE)); + if (title == null || title.equals("")) + title = fileName; + holder.titleView.setText(title); + + // upload date + if (holder.uploadDateView != null) { + String date = MediaUtils.getDate(cursor.getLong(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DATE_CREATED_GMT))); + holder.uploadDateView.setText(String.format(context.getString(R.string.media_uploaded_on), date)); + } + + // load image + if (isLocalFile) { + // should not be local file + } else { + loadNetworkImage(cursor, (NetworkImageView) holder.imageView); + } + + // get the file extension from the fileURL + String filePath = StringUtils.notNullStr(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH))); + if (filePath.isEmpty()) + filePath = StringUtils.notNullStr(cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL))); + + // file type + String fileExtension = filePath.replaceAll(".*\\.(\\w+)$", "$1").toUpperCase(); + if (holder.fileTypeView != null) { + holder.fileTypeView.setText(String.format(context.getString(R.string.media_file_type), fileExtension)); + } + + // dimensions + if (holder.dimensionView != null) { + if( MediaUtils.isValidImage(filePath)) { + int width = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_WIDTH)); + int height = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_HEIGHT)); + + if (width > 0 && height > 0) { + String dimensions = width + "x" + height; + holder.dimensionView.setText(String.format(context.getString(R.string.media_dimensions), + dimensions)); + holder.dimensionView.setVisibility(View.VISIBLE); + } + } else { + holder.dimensionView.setVisibility(View.GONE); + } + } + + } + + private void loadNetworkImage(Cursor cursor, NetworkImageView imageView) { + String thumbnailURL = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_THUMBNAIL_URL)); + if (thumbnailURL == null) { + imageView.setImageUrl(null, null); + return; + } + + Uri uri = Uri.parse(thumbnailURL); + if (uri != null && MediaUtils.isValidImage(uri.getLastPathSegment())) { + imageView.setTag(thumbnailURL); + imageView.setImageUrl(thumbnailURL, mImageLoader); + } else { + imageView.setImageUrl(null, null); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryEditFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryEditFragment.java new file mode 100644 index 000000000..e99418720 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryEditFragment.java @@ -0,0 +1,191 @@ +package org.wordpress.android.ui.media; + +import android.app.Fragment; +import android.database.Cursor; +import android.database.CursorWrapper; +import android.os.Bundle; +import android.util.SparseIntArray; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; + +import com.mobeta.android.dslv.DragSortListView; +import com.mobeta.android.dslv.DragSortListView.DropListener; +import com.mobeta.android.dslv.DragSortListView.RemoveListener; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Fragment where containing a drag-sort listview where the user can drag items + * to change their position in a media gallery + */ +public class MediaGalleryEditFragment extends Fragment implements DropListener, RemoveListener { + private static final String SAVED_MEDIA_IDS = "SAVED_MEDIA_IDS"; + private MediaGalleryAdapter mGridAdapter; + private ArrayList<String> mIds; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + + mIds = new ArrayList<String>(); + if (savedInstanceState != null) { + mIds = savedInstanceState.getStringArrayList(SAVED_MEDIA_IDS); + } + + mGridAdapter = new MediaGalleryAdapter(getActivity(), R.layout.media_gallery_item, null, true, + MediaImageLoader.getInstance()); + + View view = inflater.inflate(R.layout.media_gallery_edit_fragment, container, false); + + DragSortListView gridView = (DragSortListView) view.findViewById(R.id.edit_media_gallery_gridview); + gridView.setAdapter(mGridAdapter); + gridView.setOnCreateContextMenuListener(this); + gridView.setDropListener(this); + gridView.setRemoveListener(this); + refreshGridView(); + + return view; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putStringArrayList(SAVED_MEDIA_IDS, mIds); + } + + private void refreshGridView() { + if (WordPress.getCurrentBlog() == null) { + return; + } + + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + Cursor cursor = WordPress.wpDB.getMediaFiles(blogId, mIds); + if (cursor == null) { + mGridAdapter.changeCursor(null); + return; + } + SparseIntArray positions = mapIdsToCursorPositions(cursor); + mGridAdapter.swapCursor(new OrderedCursor(cursor, positions)); + } + + private SparseIntArray mapIdsToCursorPositions(Cursor cursor) { + SparseIntArray positions = new SparseIntArray(); + int size = mIds.size(); + for (int i = 0; i < size; i++) { + while (cursor.moveToNext()) { + String mediaId = cursor.getString(cursor.getColumnIndex("mediaId")); + if (mediaId.equals(mIds.get(i))) { + positions.put(i, cursor.getPosition()); + cursor.moveToPosition(-1); + break; + } + } + } + return positions; + } + + public void setMediaIds(ArrayList<String> ids) { + mIds = ids; + refreshGridView(); + } + + public ArrayList<String> getMediaIds() { + return mIds; + } + + public void reverseIds() { + Collections.reverse(mIds); + refreshGridView(); + } + + private class OrderedCursor extends CursorWrapper { + final int mPos; + private final int mCount; + + // a map of custom position to cursor position + private final SparseIntArray mPositions; + + /** + * A wrapper to allow for a custom order of items in a cursor * + */ + public OrderedCursor(Cursor cursor, SparseIntArray positions) { + super(cursor); + cursor.moveToPosition(-1); + mPos = 0; + mCount = cursor.getCount(); + mPositions = positions; + } + + @Override + public boolean move(int offset) { + return this.moveToPosition(this.mPos + offset); + } + + @Override + public boolean moveToNext() { + return this.moveToPosition(this.mPos + 1); + } + + @Override + public boolean moveToPrevious() { + return this.moveToPosition(this.mPos - 1); + } + + @Override + public boolean moveToFirst() { + return this.moveToPosition(0); + } + + @Override + public boolean moveToLast() { + return this.moveToPosition(this.mCount - 1); + } + + @Override + public boolean moveToPosition(int position) { + return super.moveToPosition(mPositions.get(position)); + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; + Cursor cursor = mGridAdapter.getCursor(); + if (cursor == null) { + return; + } + cursor.moveToPosition(info.position); + String mediaId = cursor.getString(cursor.getColumnIndex("mediaId")); + + menu.add(ContextMenu.NONE, mIds.indexOf(mediaId), ContextMenu.NONE, R.string.delete); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + int index = item.getItemId(); + mIds.remove(index); + refreshGridView(); + return true; + } + + @Override + public void drop(int from, int to) { + String id = mIds.get(from); + mIds.remove(id); + mIds.add(to, id); + refreshGridView(); + } + + @Override + public void remove(int position) { + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryPickerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryPickerActivity.java new file mode 100644 index 000000000..128ac806a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGalleryPickerActivity.java @@ -0,0 +1,275 @@ +package org.wordpress.android.ui.media; + +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AbsListView.MultiChoiceModeListener; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.Toast; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.util.ToastUtils; +import org.xmlrpc.android.ApiHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * An activity where the user can add new images to their media gallery or where the user + * can choose a single image to embed into their post. + */ +public class MediaGalleryPickerActivity extends AppCompatActivity + implements MultiChoiceModeListener, ActionMode.Callback, MediaGridAdapter.MediaGridAdapterCallback, + AdapterView.OnItemClickListener { + private GridView mGridView; + private MediaGridAdapter mGridAdapter; + private ActionMode mActionMode; + + private ArrayList<String> mFilteredItems; + private boolean mIsSelectOneItem; + private boolean mIsRefreshing; + private boolean mHasRetrievedAllMedia; + + private static final String STATE_FILTERED_ITEMS = "STATE_FILTERED_ITEMS"; + private static final String STATE_SELECTED_ITEMS = "STATE_SELECTED_ITEMS"; + private static final String STATE_IS_SELECT_ONE_ITEM = "STATE_IS_SELECT_ONE_ITEM"; + + public static final int REQUEST_CODE = 4000; + public static final String PARAM_SELECT_ONE_ITEM = "PARAM_SELECT_ONE_ITEM"; + private static final String PARAM_FILTERED_IDS = "PARAM_FILTERED_IDS"; + public static final String PARAM_SELECTED_IDS = "PARAM_SELECTED_IDS"; + public static final String RESULT_IDS = "RESULT_IDS"; + public static final String TAG = MediaGalleryPickerActivity.class.getSimpleName(); + + private int mOldMediaSyncOffset = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ArrayList<String> selectedItems = new ArrayList<String>(); + mFilteredItems = getIntent().getStringArrayListExtra(PARAM_FILTERED_IDS); + mIsSelectOneItem = getIntent().getBooleanExtra(PARAM_SELECT_ONE_ITEM, false); + + ArrayList<String> prevSelectedItems = getIntent().getStringArrayListExtra(PARAM_SELECTED_IDS); + if (prevSelectedItems != null) { + selectedItems.addAll(prevSelectedItems); + } + + if (savedInstanceState != null) { + selectedItems.addAll(savedInstanceState.getStringArrayList(STATE_SELECTED_ITEMS)); + mFilteredItems = savedInstanceState.getStringArrayList(STATE_FILTERED_ITEMS); + mIsSelectOneItem = savedInstanceState.getBoolean(STATE_IS_SELECT_ONE_ITEM, mIsSelectOneItem); + } + + setContentView(R.layout.media_gallery_picker_layout); + mGridView = (GridView) findViewById(R.id.media_gallery_picker_gridview); + mGridView.setMultiChoiceModeListener(this); + mGridView.setOnItemClickListener(this); + mGridAdapter = new MediaGridAdapter(this, null, 0, MediaImageLoader.getInstance()); + mGridAdapter.setSelectedItems(selectedItems); + mGridAdapter.setCallback(this); + mGridView.setAdapter(mGridAdapter); + if (mIsSelectOneItem) { + setTitle(R.string.select_from_media_library); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + } else { + mActionMode = startActionMode(this); + mActionMode.setTitle(String.format(getString(R.string.cab_selected), + mGridAdapter.getSelectedItems().size())); + } + } + + @Override + public void onResume() { + super.onResume(); + refreshViews(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putStringArrayList(STATE_SELECTED_ITEMS, mGridAdapter.getSelectedItems()); + outState.putStringArrayList(STATE_FILTERED_ITEMS, mFilteredItems); + outState.putBoolean(STATE_IS_SELECT_ONE_ITEM, mIsSelectOneItem); + } + + private void refreshViews() { + if (WordPress.getCurrentBlog() == null) + return; + final String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + Cursor cursor = WordPress.wpDB.getMediaImagesForBlog(blogId, mFilteredItems); + if (cursor.getCount() == 0) { + refreshMediaFromServer(0); + } else { + mGridAdapter.swapCursor(cursor); + } + } + + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + setResult(RESULT_CANCELED, new Intent()); + finish(); + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + if (mIsSelectOneItem) { + // Single select, just finish the activity once an item is selected + mGridAdapter.setItemSelected(position, true); + Intent intent = new Intent(); + intent.putStringArrayListExtra(RESULT_IDS, mGridAdapter.getSelectedItems()); + setResult(RESULT_OK, intent); + finish(); + } else { + mGridAdapter.toggleItemSelected(position); + mActionMode.setTitle(String.format(getString(R.string.cab_selected), + mGridAdapter.getSelectedItems().size())); + } + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + mGridAdapter.setItemSelected(position, checked); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + Intent intent = new Intent(); + intent.putStringArrayListExtra(RESULT_IDS, mGridAdapter.getSelectedItems()); + setResult(RESULT_OK, intent); + finish(); + } + + @Override + public void fetchMoreData(int offset) { + if (!mHasRetrievedAllMedia) { + refreshMediaFromServer(offset); + } + } + + @Override + public void onRetryUpload(String mediaId) { + } + + @Override + public boolean isInMultiSelect() { + return false; + } + + private void noMediaFinish() { + ToastUtils.showToast(this, R.string.media_empty_list, ToastUtils.Duration.LONG); + // Delay activity finish + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + finish(); + } + }, 1500); + } + + void refreshMediaFromServer(int offset) { + if (offset == 0 || !mIsRefreshing) { + if (offset == mOldMediaSyncOffset) { + // we're pulling the same data again for some reason. Pull from the beginning. + offset = 0; + } + mOldMediaSyncOffset = offset; + mIsRefreshing = true; + mGridAdapter.setRefreshing(true); + + List<Object> apiArgs = new ArrayList<Object>(); + apiArgs.add(WordPress.getCurrentBlog()); + + ApiHelper.SyncMediaLibraryTask.Callback callback = new ApiHelper.SyncMediaLibraryTask.Callback() { + // refersh db from server. If returned count is 0, we've retrieved all the media. + // stop retrieving until the user manually refreshes + + @Override + public void onSuccess(int count) { + MediaGridAdapter adapter = (MediaGridAdapter) mGridView.getAdapter(); + mHasRetrievedAllMedia = (count == 0); + adapter.setHasRetrievedAll(mHasRetrievedAllMedia); + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + if (WordPress.wpDB.getMediaCountAll(blogId) == 0 && count == 0) { + // There is no media at all + noMediaFinish(); + } + mIsRefreshing = false; + + // the activity may be gone by the time this finishes, so check for it + if (!isFinishing()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + //mListener.onMediaItemListDownloaded(); + mGridAdapter.setRefreshing(false); + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + Cursor cursor = WordPress.wpDB.getMediaImagesForBlog(blogId, mFilteredItems); + mGridAdapter.swapCursor(cursor); + + } + }); + } + } + + @Override + public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) { + if (errorType != ApiHelper.ErrorType.NO_ERROR) { + String message = errorType == ApiHelper.ErrorType.NO_UPLOAD_FILES_CAP + ? getString(R.string.media_error_no_permission) + : getString(R.string.error_refresh_media); + Toast.makeText(MediaGalleryPickerActivity.this, message, Toast.LENGTH_SHORT).show(); + MediaGridAdapter adapter = (MediaGridAdapter) mGridView.getAdapter(); + mHasRetrievedAllMedia = true; + adapter.setHasRetrievedAll(mHasRetrievedAllMedia); + } + + // the activity may be cone by the time we get this, so check for it + if (!isFinishing()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + mIsRefreshing = false; + mGridAdapter.setRefreshing(false); + } + }); + } + + } + }; + + ApiHelper.SyncMediaLibraryTask getMediaTask = new ApiHelper.SyncMediaLibraryTask(offset, MediaGridFragment.Filter.ALL, callback); + getMediaTask.execute(apiArgs); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGallerySettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGallerySettingsFragment.java new file mode 100644 index 000000000..e17074499 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGallerySettingsFragment.java @@ -0,0 +1,370 @@ +package org.wordpress.android.ui.media; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.util.SparseBooleanArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.ScrollView; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.ui.ExpandableHeightGridView; + +import java.util.ArrayList; + +/** + * The fragment containing the settings for the media gallery + */ +public class MediaGallerySettingsFragment extends Fragment implements OnCheckedChangeListener { + private static final int DEFAULT_THUMBNAIL_COLUMN_COUNT = 3; + + private static final String STATE_NUM_COLUMNS = "STATE_NUM_COLUMNS"; + private static final String STATE_GALLERY_TYPE_ORD = "GALLERY_TYPE_ORD"; + private static final String STATE_RANDOM_ORDER = "STATE_RANDOM_ORDER"; + + private CheckBox mThumbnailCheckbox; + private CheckBox mSquaresCheckbox; + private CheckBox mTiledCheckbox; + private CheckBox mCirclesCheckbox; + private CheckBox mSlideshowCheckbox; + + private GalleryType mType; + private int mNumColumns; + private boolean mIsRandomOrder; + + private View mNumColumnsContainer; + private View mHeader; + + private CheckBox mRandomOrderCheckbox; + + private boolean mAllowCheckChanged; + + private TextView mTitleView; + + private ScrollView mScrollView; + + private CustomGridAdapter mGridAdapter; + + private MediaGallerySettingsCallback mCallback; + + + private enum GalleryType { + DEFAULT(""), + TILED("rectangular"), + SQUARES("square"), + CIRCLES("circle"), + SLIDESHOW("slideshow"); + + private final String mTag; + + private GalleryType(String tag) { + mTag = tag; + } + + public String getTag() { + return mTag; + } + + public static GalleryType getTypeFromTag(String tag) { + if (tag.equals("rectangular")) + return TILED; + else if (tag.equals("square")) + return SQUARES; + else if (tag.equals("circle")) + return CIRCLES; + else if (tag.equals("slideshow")) + return SLIDESHOW; + else + return DEFAULT; + } + + } + + public interface MediaGallerySettingsCallback { + public void onReverseClicked(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mCallback = (MediaGallerySettingsCallback) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must implement MediaGallerySettingsCallback"); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mAllowCheckChanged = true; + mType = GalleryType.DEFAULT; + mNumColumns = DEFAULT_THUMBNAIL_COLUMN_COUNT; + mIsRandomOrder = false; + + restoreState(savedInstanceState); + + View view = inflater.inflate(R.layout.media_gallery_settings_fragment, container, false); + + mHeader = view.findViewById(R.id.media_gallery_settings_header); + mScrollView = (ScrollView) view.findViewById(R.id.media_gallery_settings_content_container); + mTitleView = (TextView) view.findViewById(R.id.media_gallery_settings_title); + + mNumColumnsContainer = view.findViewById(R.id.media_gallery_settings_num_columns_container); + int visible = (mType == GalleryType.DEFAULT) ? View.VISIBLE : View.GONE; + mNumColumnsContainer.setVisibility(visible); + + ExpandableHeightGridView numColumnsGrid = (ExpandableHeightGridView) view.findViewById(R.id.media_gallery_num_columns_grid); + numColumnsGrid.setExpanded(true); + ArrayList<String> list = new ArrayList<String>(9); + for (int i = 1; i <= 9; i++) { + list.add(i + ""); + } + + mGridAdapter = new CustomGridAdapter(mNumColumns); + numColumnsGrid.setAdapter(mGridAdapter); + + mThumbnailCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_thumbnail_grid); + mTiledCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_tiled); + mSquaresCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_squares); + mCirclesCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_circles); + mSlideshowCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_type_slideshow); + + setType(mType.getTag()); + + mThumbnailCheckbox.setOnCheckedChangeListener(this); + mTiledCheckbox.setOnCheckedChangeListener(this); + mSquaresCheckbox.setOnCheckedChangeListener(this); + mCirclesCheckbox.setOnCheckedChangeListener(this); + + mSlideshowCheckbox.setOnCheckedChangeListener(this); + + mRandomOrderCheckbox = (CheckBox) view.findViewById(R.id.media_gallery_random_checkbox); + mRandomOrderCheckbox.setChecked(mIsRandomOrder); + mRandomOrderCheckbox.setOnCheckedChangeListener(this); + + Button reverseButton = (Button) view.findViewById(R.id.media_gallery_settings_reverse_button); + reverseButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mCallback.onReverseClicked(); + } + }); + + return view; + } + + private void restoreState(Bundle savedInstanceState) { + if (savedInstanceState == null) + return; + + mNumColumns = savedInstanceState.getInt(STATE_NUM_COLUMNS); + int galleryTypeOrdinal = savedInstanceState.getInt(STATE_GALLERY_TYPE_ORD); + mType = GalleryType.values()[galleryTypeOrdinal]; + mIsRandomOrder = savedInstanceState.getBoolean(STATE_RANDOM_ORDER); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(STATE_NUM_COLUMNS, mNumColumns); + outState.putBoolean(STATE_RANDOM_ORDER, mIsRandomOrder); + outState.putInt(STATE_GALLERY_TYPE_ORD, mType.ordinal()); + } + + @Override + public void onCheckedChanged(CompoundButton button, boolean checked) { + if (!mAllowCheckChanged) + return; + + // the checkboxes for types are mutually exclusive, so when one is set, + // the others must be unset. Disable the checkChange listener during this time, + // and re-enable it once done. + mAllowCheckChanged = false; + + int numColumnsContainerVisible = View.GONE; + int i = button.getId(); + if (i == R.id.media_gallery_type_thumbnail_grid) { + numColumnsContainerVisible = View.VISIBLE; + + mType = GalleryType.DEFAULT; + mThumbnailCheckbox.setChecked(true); + mSquaresCheckbox.setChecked(false); + mTiledCheckbox.setChecked(false); + mCirclesCheckbox.setChecked(false); + mSlideshowCheckbox.setChecked(false); + } else if (i == R.id.media_gallery_type_tiled) { + mType = GalleryType.TILED; + mThumbnailCheckbox.setChecked(false); + mTiledCheckbox.setChecked(true); + mSquaresCheckbox.setChecked(false); + mCirclesCheckbox.setChecked(false); + mSlideshowCheckbox.setChecked(false); + } else if (i == R.id.media_gallery_type_squares) { + mType = GalleryType.SQUARES; + mThumbnailCheckbox.setChecked(false); + mTiledCheckbox.setChecked(false); + mSquaresCheckbox.setChecked(true); + mCirclesCheckbox.setChecked(false); + mSlideshowCheckbox.setChecked(false); + } else if (i == R.id.media_gallery_type_circles) { + mType = GalleryType.CIRCLES; + mThumbnailCheckbox.setChecked(false); + mSquaresCheckbox.setChecked(false); + mTiledCheckbox.setChecked(false); + mCirclesCheckbox.setChecked(true); + mSlideshowCheckbox.setChecked(false); + } else if (i == R.id.media_gallery_type_slideshow) { + mType = GalleryType.SLIDESHOW; + mThumbnailCheckbox.setChecked(false); + mSquaresCheckbox.setChecked(false); + mTiledCheckbox.setChecked(false); + mCirclesCheckbox.setChecked(false); + mSlideshowCheckbox.setChecked(true); + } else if (i == R.id.media_gallery_random_checkbox) { + numColumnsContainerVisible = mNumColumnsContainer.getVisibility(); + mIsRandomOrder = checked; + } + + mNumColumnsContainer.setVisibility(numColumnsContainerVisible); + + mAllowCheckChanged = true; + } + + private class CustomGridAdapter extends BaseAdapter implements OnCheckedChangeListener { + private boolean mAllowCheckChanged; + private final SparseBooleanArray mCheckedPositions; + + public CustomGridAdapter(int selection) { + mAllowCheckChanged = true; + mCheckedPositions = new SparseBooleanArray(9); + setSelection(selection); + } + + // when a number of columns is checked, the numbers less than + // the one chose are also set to checked on the ui. + // e.g. when 3 is checked, 1 and 2 are as well. + private void setSelection(int selection) { + for (int i = 0; i < 9; i++){ + if (i + 1 <= selection) + mCheckedPositions.put(i, true); + else + mCheckedPositions.put(i, false); + } + } + + @Override + public int getCount() { + return 9; + } + + @Override + public Object getItem(int position) { + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + CheckBox checkbox = (CheckBox) inflater.inflate(R.layout.media_gallery_column_checkbox, parent, false); + checkbox.setChecked(mCheckedPositions.get(position)); + checkbox.setTag(position); + checkbox.setText(String.valueOf(position + 1)); + checkbox.setOnCheckedChangeListener(this); + + return checkbox; + } + + @Override + public void onCheckedChanged(CompoundButton button, boolean checked) { + if (mAllowCheckChanged) { + mAllowCheckChanged = false; + + int position = (Integer) button.getTag(); + mNumColumns = position + 1; + + int count = mCheckedPositions.size(); + for (int i = 0; i < count; i++) { + if (i <= position) + mCheckedPositions.put(i, true); + else + mCheckedPositions.put(i, false); + } + notifyDataSetChanged(); + mAllowCheckChanged = true; + } + } + + } + + public View getDragView() { + return mHeader; + } + + public void onPanelExpanded() { + mTitleView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.gallery_arrow_dropdown_open, 0); + mScrollView.scrollTo(0, 0); + } + + public void onPanelCollapsed() { + mTitleView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.gallery_arrow_dropdown_closed, 0); + } + + public void setRandom(boolean random) { + mIsRandomOrder = random; + mRandomOrderCheckbox.setChecked(mIsRandomOrder); + } + + public boolean isRandom() { + return mIsRandomOrder; + } + + public void setType(String type) { + mType = GalleryType.getTypeFromTag(type); + switch (mType) { + case CIRCLES: + mCirclesCheckbox.setChecked(true); + break; + case DEFAULT: + mThumbnailCheckbox.setChecked(true); + break; + case SLIDESHOW: + mSlideshowCheckbox.setChecked(true); + break; + case SQUARES: + mSquaresCheckbox.setChecked(true); + break; + case TILED: + mTiledCheckbox.setChecked(true); + break; + } + } + + public String getType() { + return mType.getTag(); + } + + public void setNumColumns(int numColumns) { + mNumColumns = numColumns; + mGridAdapter.setSelection(numColumns); + mGridAdapter.notifyDataSetChanged(); + } + + public int getNumColumns() { + return mNumColumns; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java new file mode 100644 index 000000000..61192a4a1 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridAdapter.java @@ -0,0 +1,519 @@ +package org.wordpress.android.ui.media; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.MergeCursor; +import android.graphics.Bitmap; +import android.os.Handler; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.CursorAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.NetworkImageView; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.WordPressDB; +import org.wordpress.android.ui.CheckableFrameLayout; +import org.wordpress.android.util.DisplayUtils; +import org.wordpress.android.util.ImageUtils.BitmapWorkerCallback; +import org.wordpress.android.util.ImageUtils.BitmapWorkerTask; +import org.wordpress.android.util.MediaUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * An adapter for the media gallery listViews. + */ +public class MediaGridAdapter extends CursorAdapter { + private MediaGridAdapterCallback mCallback; + private boolean mHasRetrievedAll; + private boolean mIsRefreshing; + private int mCursorDataCount; + private int mGridItemWidth; + private final Map<String, List<BitmapReadyCallback>> mFilePathToCallbackMap; + private final Handler mHandler; + private final int mLocalImageWidth; + private final LayoutInflater mInflater; + private ImageLoader mImageLoader; + private Context mContext; + // Must be an ArrayList (order is important for galleries) + private ArrayList<String> mSelectedItems; + + public interface MediaGridAdapterCallback { + public void fetchMoreData(int offset); + public void onRetryUpload(String mediaId); + public boolean isInMultiSelect(); + } + + interface BitmapReadyCallback { + void onBitmapReady(Bitmap bitmap); + } + + private static enum ViewTypes { + LOCAL, NETWORK, PROGRESS, SPACER + } + + public MediaGridAdapter(Context context, Cursor c, int flags, ImageLoader imageLoader) { + super(context, c, flags); + mContext = context; + mSelectedItems = new ArrayList<String>(); + mLocalImageWidth = context.getResources().getDimensionPixelSize(R.dimen.media_grid_local_image_width); + mInflater = LayoutInflater.from(context); + mFilePathToCallbackMap = new HashMap<String, List<BitmapReadyCallback>>(); + mHandler = new Handler(); + setImageLoader(imageLoader); + } + + void setImageLoader(ImageLoader imageLoader) { + if (imageLoader != null) { + mImageLoader = imageLoader; + } else { + mImageLoader = WordPress.imageLoader; + } + } + + public ArrayList<String> getSelectedItems() { + return mSelectedItems; + } + + private static class GridViewHolder { + private final TextView filenameView; + private final TextView titleView; + private final TextView uploadDateView; + private final ImageView imageView; + private final TextView fileTypeView; + private final TextView dimensionView; + private final CheckableFrameLayout frameLayout; + + private final TextView stateTextView; + private final ProgressBar progressUpload; + private final RelativeLayout uploadStateView; + + GridViewHolder(View view) { + filenameView = (TextView) view.findViewById(R.id.media_grid_item_filename); + titleView = (TextView) view.findViewById(R.id.media_grid_item_name); + uploadDateView = (TextView) view.findViewById(R.id.media_grid_item_upload_date); + imageView = (ImageView) view.findViewById(R.id.media_grid_item_image); + fileTypeView = (TextView) view.findViewById(R.id.media_grid_item_filetype); + dimensionView = (TextView) view.findViewById(R.id.media_grid_item_dimension); + frameLayout = (CheckableFrameLayout) view.findViewById(R.id.media_grid_frame_layout); + + stateTextView = (TextView) view.findViewById(R.id.media_grid_item_upload_state); + progressUpload = (ProgressBar) view.findViewById(R.id.media_grid_item_upload_progress); + uploadStateView = (RelativeLayout) view.findViewById(R.id.media_grid_item_upload_state_container); + } + } + + @SuppressLint("DefaultLocale") + @Override + public void bindView(final View view, Context context, Cursor cursor) { + int itemViewType = getItemViewType(cursor.getPosition()); + + if (itemViewType == ViewTypes.PROGRESS.ordinal()) { + if (mIsRefreshing) { + int height = mContext.getResources().getDimensionPixelSize(R.dimen.media_grid_progress_height); + view.setLayoutParams(new GridView.LayoutParams(GridView.LayoutParams.MATCH_PARENT, height)); + view.setVisibility(View.VISIBLE); + } else { + view.setLayoutParams(new GridView.LayoutParams(0, 0)); + view.setVisibility(View.GONE); + } + return; + } else if (itemViewType == ViewTypes.SPACER.ordinal()) { + CheckableFrameLayout frameLayout = (CheckableFrameLayout) view.findViewById(R.id.media_grid_frame_layout); + updateGridWidth(context, frameLayout); + view.setVisibility(View.INVISIBLE); + return; + } + + final GridViewHolder holder; + if (view.getTag() instanceof GridViewHolder) { + holder = (GridViewHolder) view.getTag(); + } else { + holder = new GridViewHolder(view); + view.setTag(holder); + } + + final String mediaId = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID)); + + String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE)); + boolean isLocalFile = MediaUtils.isLocalFile(state); + + // file name + String fileName = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME)); + if (holder.filenameView != null) { + holder.filenameView.setText(fileName); + } + + // title of media + String title = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_TITLE)); + if (title == null || title.equals("")) + title = fileName; + holder.titleView.setText(title); + + // upload date + if (holder.uploadDateView != null) { + String date = MediaUtils.getDate(cursor.getLong(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DATE_CREATED_GMT))); + holder.uploadDateView.setText(date); + } + + // load image + if (isLocalFile) { + loadLocalImage(cursor, holder.imageView); + } else { + String thumbUrl = WordPressMediaUtils.getNetworkThumbnailUrl(cursor, mGridItemWidth); + WordPressMediaUtils.loadNetworkImage(thumbUrl, (NetworkImageView) holder.imageView, mImageLoader); + } + + // get the file extension from the fileURL + String mimeType = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MIME_TYPE)); + String fileExtension = MediaUtils.getExtensionForMimeType(mimeType); + fileExtension = fileExtension.toUpperCase(); + // file type + if (DisplayUtils.isXLarge(context) && !TextUtils.isEmpty(fileExtension)) { + holder.fileTypeView.setText(String.format(context.getString(R.string.media_file_type), fileExtension)); + } else { + holder.fileTypeView.setText(fileExtension); + } + + // dimensions + String filePath = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL)); + TextView dimensionView = (TextView) view.findViewById(R.id.media_grid_item_dimension); + if (dimensionView != null) { + if( MediaUtils.isValidImage(filePath)) { + int width = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_WIDTH)); + int height = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_HEIGHT)); + + if (width > 0 && height > 0) { + String dimensions = width + "x" + height; + holder.dimensionView.setText(dimensions); + holder.dimensionView.setVisibility(View.VISIBLE); + } + } else { + holder.dimensionView.setVisibility(View.GONE); + } + } + + holder.frameLayout.setTag(mediaId); + holder.frameLayout.setChecked(mSelectedItems.contains(mediaId)); + + // resizing layout to fit nicely into grid view + updateGridWidth(context, holder.frameLayout); + + // show upload state + if (holder.stateTextView != null) { + if (state != null && state.length() > 0) { + // show the progressbar only when the state is uploading + if (state.equals("uploading")) { + holder.progressUpload.setVisibility(View.VISIBLE); + } else { + holder.progressUpload.setVisibility(View.GONE); + if (state.equals("uploaded")) { + holder.stateTextView.setVisibility(View.GONE); + } + } + + // add onclick to retry failed uploads + if (state.equals("failed")) { + state = "retry"; + holder.stateTextView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (!inMultiSelect()) { + ((TextView) v).setText(R.string.upload_queued); + v.setOnClickListener(null); + mCallback.onRetryUpload(mediaId); + } + } + + }); + } + + holder.stateTextView.setText(state); + holder.uploadStateView.setVisibility(View.VISIBLE); + } else { + holder.uploadStateView.setVisibility(View.GONE); + } + } + + // if we are near the end, make a call to fetch more + int position = cursor.getPosition(); + if (position == mCursorDataCount - 1 && !mHasRetrievedAll) { + if (mCallback != null) { + mCallback.fetchMoreData(mCursorDataCount); + } + } + } + + private boolean inMultiSelect() { + return mCallback.isInMultiSelect(); + } + + private synchronized void loadLocalImage(Cursor cursor, final ImageView imageView) { + final String filePath = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH)); + + if (MediaUtils.isValidImage(filePath)) { + imageView.setTag(filePath); + + Bitmap bitmap = WordPress.getBitmapCache().get(filePath); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } else { + imageView.setImageBitmap(null); + + boolean shouldFetch = false; + + List<BitmapReadyCallback> list; + if (mFilePathToCallbackMap.containsKey(filePath)) { + list = mFilePathToCallbackMap.get(filePath); + } else { + list = new ArrayList<MediaGridAdapter.BitmapReadyCallback>(); + shouldFetch = true; + mFilePathToCallbackMap.put(filePath, list); + } + list.add(new BitmapReadyCallback() { + @Override + public void onBitmapReady(Bitmap bitmap) { + if (imageView.getTag() instanceof String && imageView.getTag().equals(filePath)) + imageView.setImageBitmap(bitmap); + } + }); + + + if (shouldFetch) { + fetchBitmap(filePath); + } + } + } else { + // if not image, for now show no image. + imageView.setImageBitmap(null); + } + } + + private void fetchBitmap(final String filePath) { + BitmapWorkerTask task = new BitmapWorkerTask(null, mLocalImageWidth, mLocalImageWidth, new BitmapWorkerCallback() { + @Override + public void onBitmapReady(final String path, ImageView imageView, final Bitmap bitmap) { + mHandler.post(new Runnable() { + @Override + public void run() { + List<BitmapReadyCallback> callbacks = mFilePathToCallbackMap.get(path); + for (BitmapReadyCallback callback : callbacks) { + callback.onBitmapReady(bitmap); + } + + WordPress.getBitmapCache().put(path, bitmap); + callbacks.clear(); + mFilePathToCallbackMap.remove(path); + } + }); + } + }); + task.execute(filePath); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup root) { + int itemViewType = getItemViewType(cursor.getPosition()); + + // spacer and progress spinner views + if (itemViewType == ViewTypes.PROGRESS.ordinal()) { + return mInflater.inflate(R.layout.media_grid_progress, root, false); + } else if (itemViewType == ViewTypes.SPACER.ordinal()) { + return mInflater.inflate(R.layout.media_grid_item, root, false); + } + + View view = mInflater.inflate(R.layout.media_grid_item, root, false); + ViewStub imageStub = (ViewStub) view.findViewById(R.id.media_grid_image_stub); + + // We need to use ViewStubs to inflate the image to either: + // - a regular ImageView (for local images) + // - a FadeInNetworkImageView (for network images) + // This is because the NetworkImageView can't load local images. + // The other option would be to inflate multiple layouts, but that would lead + // to extra near-duplicate xml files that would need to be maintained. + if (itemViewType == ViewTypes.LOCAL.ordinal()) { + imageStub.setLayoutResource(R.layout.media_grid_image_local); + } else { + imageStub.setLayoutResource(R.layout.media_grid_image_network); + } + + imageStub.inflate(); + + view.setTag(new GridViewHolder(view)); + + return view; + } + + @Override + public int getViewTypeCount() { + return ViewTypes.values().length; + } + + @Override + public int getItemViewType(int position) { + Cursor cursor = getCursor(); + cursor.moveToPosition(position); + + // spacer / progress cells + int _id = cursor.getInt(cursor.getColumnIndex("_id")); + if (_id < 0) { + if (_id == Integer.MIN_VALUE) + return ViewTypes.PROGRESS.ordinal(); + else + return ViewTypes.SPACER.ordinal(); + } + + // regular cells + String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE)); + if (MediaUtils.isLocalFile(state)) + return ViewTypes.LOCAL.ordinal(); + else + return ViewTypes.NETWORK.ordinal(); + } + + /** Updates the width of a cell to max out the space available, for phones **/ + private void updateGridWidth(Context context, View view) { + setGridItemWidth(); + int columnCount = getColumnCount(context); + + if (columnCount > 1) { + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mGridItemWidth, mGridItemWidth); + view.setLayoutParams(params); + } + } + + @Override + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == null) { + mCursorDataCount = 0; + return super.swapCursor(newCursor); + } + + mCursorDataCount = newCursor.getCount(); + + // to mimic the infinite the notification's infinite scroll ui + // (with a progress spinner on the bottom of the list), we'll need to add + // extra cells in the gridview: + // - spacer cells as fillers to place the progress spinner on the first cell (_id < 0) + // - progress spinner cell (_id = Integer.MIN_VALUE) + + // use a matrix cursor to create the extra rows + MatrixCursor matrixCursor = new MatrixCursor(new String[] { "_id" }); + + // add spacer cells + int columnCount = getColumnCount(mContext); + int remainder = newCursor.getCount() % columnCount; + if (remainder > 0) { + int spaceCount = columnCount - remainder; + for (int i = 0; i < spaceCount; i++ ) { + int id = i - spaceCount; + matrixCursor.addRow(new Object[] {id + ""}); + } + } + + // add progress spinner cell + matrixCursor.addRow(new Object[] { Integer.MIN_VALUE }); + + // use a merge cursor to place merge the extra rows at the bottom of the newly swapped cursor + MergeCursor mergeCursor = new MergeCursor(new Cursor[] { newCursor, matrixCursor }); + return super.swapCursor(mergeCursor); + } + + /** Return the number of columns in the media grid **/ + private int getColumnCount(Context context) { + return context.getResources().getInteger(R.integer.media_grid_num_columns); + } + + public void setCallback(MediaGridAdapterCallback callback) { + mCallback = callback; + } + + public void setHasRetrievedAll(boolean b) { + mHasRetrievedAll = b; + } + + public void setRefreshing(boolean refreshing) { + mIsRefreshing = refreshing; + notifyDataSetChanged(); + } + + public int getDataCount() { + return mCursorDataCount; + } + + private void setGridItemWidth() { + int maxWidth = mContext.getResources().getDisplayMetrics().widthPixels; + int columnCount = getColumnCount(mContext); + if (columnCount > 0) { + int dp8 = DisplayUtils.dpToPx(mContext, 8); + int padding = (columnCount + 1) * dp8; + mGridItemWidth = (maxWidth - padding) / columnCount; + } + } + + public void clearSelection() { + mSelectedItems.clear(); + } + + public boolean isItemSelected(String mediaId) { + return mSelectedItems.contains(mediaId); + } + + public void setItemSelected(int position, boolean selected) { + Cursor cursor = (Cursor) getItem(position); + if (cursor == null) { + return; + } + int columnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID); + if (columnIndex != -1) { + String mediaId = cursor.getString(columnIndex); + setItemSelected(mediaId, selected); + } + } + + public void setItemSelected(String mediaId, boolean selected) { + if (selected) { + mSelectedItems.add(mediaId); + } else { + mSelectedItems.remove(mediaId); + } + notifyDataSetChanged(); + } + + public void toggleItemSelected(int position) { + Cursor cursor = (Cursor) getItem(position); + int columnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID); + if (columnIndex != -1) { + String mediaId = cursor.getString(columnIndex); + if (mSelectedItems.contains(mediaId)) { + mSelectedItems.remove(mediaId); + } else { + mSelectedItems.add(mediaId); + } + notifyDataSetChanged(); + } + } + + public void setSelectedItems(ArrayList<String> selectedItems) { + mSelectedItems = selectedItems; + notifyDataSetChanged(); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridFragment.java new file mode 100644 index 000000000..793a8e647 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaGridFragment.java @@ -0,0 +1,836 @@ +package org.wordpress.android.ui.media; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.Fragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AbsListView.RecyclerListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.DatePicker; +import android.widget.GridView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.volley.VolleyError; +import com.android.volley.toolbox.ImageLoader.ImageContainer; +import com.android.volley.toolbox.ImageLoader.ImageListener; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.Blog; +import org.wordpress.android.ui.CheckableFrameLayout; +import org.wordpress.android.ui.CustomSpinner; +import org.wordpress.android.ui.EmptyViewMessageType; +import org.wordpress.android.ui.media.MediaGridAdapter.MediaGridAdapterCallback; +import org.wordpress.android.ui.posts.EditPostActivity; +import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.ToastUtils.Duration; +import org.wordpress.android.util.WPActivityUtils; +import org.wordpress.android.util.helpers.SwipeToRefreshHelper; +import org.wordpress.android.util.helpers.SwipeToRefreshHelper.RefreshListener; +import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout; +import org.xmlrpc.android.ApiHelper; +import org.xmlrpc.android.ApiHelper.SyncMediaLibraryTask.Callback; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; + +/** + * The grid displaying the media items. + */ +public class MediaGridFragment extends Fragment + implements OnItemClickListener, MediaGridAdapterCallback, RecyclerListener { + private static final String BUNDLE_SELECTED_STATES = "BUNDLE_SELECTED_STATES"; + private static final String BUNDLE_IN_MULTI_SELECT_MODE = "BUNDLE_IN_MULTI_SELECT_MODE"; + private static final String BUNDLE_SCROLL_POSITION = "BUNDLE_SCROLL_POSITION"; + private static final String BUNDLE_HAS_RETREIEVED_ALL_MEDIA = "BUNDLE_HAS_RETREIEVED_ALL_MEDIA"; + private static final String BUNDLE_FILTER = "BUNDLE_FILTER"; + private static final String BUNDLE_EMPTY_VIEW_MESSAGE = "BUNDLE_EMPTY_VIEW_MESSAGE"; + + private static final String BUNDLE_DATE_FILTER_SET = "BUNDLE_DATE_FILTER_SET"; + private static final String BUNDLE_DATE_FILTER_VISIBLE = "BUNDLE_DATE_FILTER_VISIBLE"; + private static final String BUNDLE_DATE_FILTER_START_YEAR = "BUNDLE_DATE_FILTER_START_YEAR"; + private static final String BUNDLE_DATE_FILTER_START_MONTH = "BUNDLE_DATE_FILTER_START_MONTH"; + private static final String BUNDLE_DATE_FILTER_START_DAY = "BUNDLE_DATE_FILTER_START_DAY"; + private static final String BUNDLE_DATE_FILTER_END_YEAR = "BUNDLE_DATE_FILTER_END_YEAR"; + private static final String BUNDLE_DATE_FILTER_END_MONTH = "BUNDLE_DATE_FILTER_END_MONTH"; + private static final String BUNDLE_DATE_FILTER_END_DAY = "BUNDLE_DATE_FILTER_END_DAY"; + + private Filter mFilter = Filter.ALL; + private String[] mFiltersText; + private GridView mGridView; + private MediaGridAdapter mGridAdapter; + private MediaGridListener mListener; + + private boolean mIsRefreshing; + private boolean mHasRetrievedAllMedia; + private boolean mIsMultiSelect; + private String mSearchTerm; + + private View mSpinnerContainer; + private TextView mResultView; + private CustomSpinner mSpinner; + private SwipeToRefreshHelper mSwipeToRefreshHelper; + + private LinearLayout mEmptyView; + private TextView mEmptyViewTitle; + private EmptyViewMessageType mEmptyViewMessageType = EmptyViewMessageType.NO_CONTENT; + + private int mOldMediaSyncOffset = 0; + + private boolean mIsDateFilterSet; + private boolean mSpinnerHasLaunched; + + private int mStartYear, mStartMonth, mStartDay, mEndYear, mEndMonth, mEndDay; + private AlertDialog mDatePickerDialog; + + public interface MediaGridListener { + public void onMediaItemListDownloadStart(); + public void onMediaItemListDownloaded(); + public void onMediaItemSelected(String mediaId); + public void onRetryUpload(String mediaId); + } + + public enum Filter { + ALL, IMAGES, UNATTACHED, CUSTOM_DATE; + + public static Filter getFilter(int filterPos) { + if (filterPos > Filter.values().length) + return ALL; + else + return Filter.values()[filterPos]; + } + } + + private final OnItemSelectedListener mFilterSelectedListener = new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + // need this to stop the bug where onItemSelected is called during initialization, before user input + if (!mSpinnerHasLaunched) { + return; + } + if (position == Filter.CUSTOM_DATE.ordinal()) { + mIsDateFilterSet = true; + } + setFilter(Filter.getFilter(position)); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + }; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + mFiltersText = new String[Filter.values().length]; + mGridAdapter = new MediaGridAdapter(getActivity(), null, 0, MediaImageLoader.getInstance()); + mGridAdapter.setCallback(this); + + View view = inflater.inflate(R.layout.media_grid_fragment, container); + + mGridView = (GridView) view.findViewById(R.id.media_gridview); + mGridView.setOnItemClickListener(this); + mGridView.setRecyclerListener(this); + mGridView.setMultiChoiceModeListener(new MultiChoiceModeListener()); + mGridView.setChoiceMode(GridView.CHOICE_MODE_MULTIPLE_MODAL); + mGridView.setAdapter(mGridAdapter); + + mEmptyView = (LinearLayout) view.findViewById(R.id.empty_view); + mEmptyViewTitle = (TextView) view.findViewById(R.id.empty_view_title); + + mResultView = (TextView) view.findViewById(R.id.media_filter_result_text); + + mSpinner = (CustomSpinner) view.findViewById(R.id.media_filter_spinner); + mSpinner.setOnItemSelectedListener(mFilterSelectedListener); + mSpinner.setOnItemSelectedEvenIfUnchangedListener(mFilterSelectedListener); + + mSpinnerContainer = view.findViewById(R.id.media_filter_spinner_container); + mSpinnerContainer.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (!isInMultiSelect()) { + mSpinnerHasLaunched = true; + mSpinner.performClick(); + } + } + + }); + + // swipe to refresh setup + mSwipeToRefreshHelper = new SwipeToRefreshHelper(getActivity(), + (CustomSwipeRefreshLayout) view.findViewById(R.id.ptr_layout), + new RefreshListener() { + @Override + public void onRefreshStarted() { + if (!isAdded()) { + return; + } + if (!NetworkUtils.checkConnection(getActivity())) { + updateEmptyView(EmptyViewMessageType.NETWORK_ERROR); + mSwipeToRefreshHelper.setRefreshing(false); + return; + } + refreshMediaFromServer(0, false); + } + }); + restoreState(savedInstanceState); + setupSpinnerAdapter(); + + return view; + } + + private void restoreState(Bundle savedInstanceState) { + if (savedInstanceState == null) + return; + + boolean isInMultiSelectMode = savedInstanceState.getBoolean(BUNDLE_IN_MULTI_SELECT_MODE); + + if (savedInstanceState.containsKey(BUNDLE_SELECTED_STATES)) { + ArrayList selectedItems = savedInstanceState.getStringArrayList(BUNDLE_SELECTED_STATES); + mGridAdapter.setSelectedItems(selectedItems); + if (isInMultiSelectMode) { + setFilterSpinnerVisible(mGridAdapter.getSelectedItems().size() == 0); + mSwipeToRefreshHelper.setEnabled(false); + } + } + + mGridView.setSelection(savedInstanceState.getInt(BUNDLE_SCROLL_POSITION, 0)); + mHasRetrievedAllMedia = savedInstanceState.getBoolean(BUNDLE_HAS_RETREIEVED_ALL_MEDIA, false); + mFilter = Filter.getFilter(savedInstanceState.getInt(BUNDLE_FILTER)); + mEmptyViewMessageType = EmptyViewMessageType.getEnumFromString(savedInstanceState. + getString(BUNDLE_EMPTY_VIEW_MESSAGE)); + + mIsDateFilterSet = savedInstanceState.getBoolean(BUNDLE_DATE_FILTER_SET, false); + mStartDay = savedInstanceState.getInt(BUNDLE_DATE_FILTER_START_DAY); + mStartMonth = savedInstanceState.getInt(BUNDLE_DATE_FILTER_START_MONTH); + mStartYear = savedInstanceState.getInt(BUNDLE_DATE_FILTER_START_YEAR); + mEndDay = savedInstanceState.getInt(BUNDLE_DATE_FILTER_END_DAY); + mEndMonth = savedInstanceState.getInt(BUNDLE_DATE_FILTER_END_MONTH); + mEndYear = savedInstanceState.getInt(BUNDLE_DATE_FILTER_END_YEAR); + + boolean datePickerShowing = savedInstanceState.getBoolean(BUNDLE_DATE_FILTER_VISIBLE); + if (datePickerShowing) + showDatePicker(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + saveState(outState); + } + + private void saveState(Bundle outState) { + outState.putStringArrayList(BUNDLE_SELECTED_STATES, mGridAdapter.getSelectedItems()); + outState.putInt(BUNDLE_SCROLL_POSITION, mGridView.getFirstVisiblePosition()); + outState.putBoolean(BUNDLE_HAS_RETREIEVED_ALL_MEDIA, mHasRetrievedAllMedia); + outState.putBoolean(BUNDLE_IN_MULTI_SELECT_MODE, isInMultiSelect()); + outState.putInt(BUNDLE_FILTER, mFilter.ordinal()); + outState.putString(BUNDLE_EMPTY_VIEW_MESSAGE, mEmptyViewMessageType.name()); + + outState.putBoolean(BUNDLE_DATE_FILTER_SET, mIsDateFilterSet); + outState.putBoolean(BUNDLE_DATE_FILTER_VISIBLE, (mDatePickerDialog != null && mDatePickerDialog.isShowing())); + outState.putInt(BUNDLE_DATE_FILTER_START_DAY, mStartDay); + outState.putInt(BUNDLE_DATE_FILTER_START_MONTH, mStartMonth); + outState.putInt(BUNDLE_DATE_FILTER_START_YEAR, mStartYear); + outState.putInt(BUNDLE_DATE_FILTER_END_DAY, mEndDay); + outState.putInt(BUNDLE_DATE_FILTER_END_MONTH, mEndMonth); + outState.putInt(BUNDLE_DATE_FILTER_END_YEAR, mEndYear); + } + + private void setupSpinnerAdapter() { + if (getActivity() == null || WordPress.getCurrentBlog() == null) { + return; + } + + updateFilterText(); + + Context context = WPActivityUtils.getThemedContext(getActivity()); + ArrayAdapter<String> adapter = new ArrayAdapter<String>(context, R.layout.spinner_menu_dropdown_item, mFiltersText); + mSpinner.setAdapter(adapter); + mSpinner.setSelection(mFilter.ordinal()); + } + + public void refreshSpinnerAdapter() { + updateFilterText(); + updateSpinnerAdapter(); + setFilter(mFilter); + } + + void resetSpinnerAdapter() { + setFiltersText(0, 0, 0); + updateSpinnerAdapter(); + } + + void updateFilterText() { + if (WordPress.currentBlog == null) + return; + + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + + int countAll = WordPress.wpDB.getMediaCountAll(blogId); + int countImages = WordPress.wpDB.getMediaCountImages(blogId); + int countUnattached = WordPress.wpDB.getMediaCountUnattached(blogId); + + setFiltersText(countAll, countImages, countUnattached); + } + + private void setFiltersText(int countAll, int countImages, int countUnattached) { + mFiltersText[0] = getResources().getString(R.string.all) + " (" + countAll + ")"; + mFiltersText[1] = getResources().getString(R.string.images) + " (" + countImages + ")"; + mFiltersText[2] = getResources().getString(R.string.unattached) + " (" + countUnattached + ")"; + mFiltersText[3] = getResources().getString(R.string.custom_date) + "..."; + } + + void updateSpinnerAdapter() { + ArrayAdapter<String> adapter = (ArrayAdapter<String>) mSpinner.getAdapter(); + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mListener = (MediaGridListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must implement MediaGridListener"); + } + } + + @Override + public void onResume() { + super.onResume(); + refreshSpinnerAdapter(); + refreshMediaFromDB(); + } + + public void refreshMediaFromDB() { + setFilter(mFilter); + if (isAdded() && mGridAdapter.getDataCount() == 0) { + if (NetworkUtils.isNetworkAvailable(getActivity())) { + if (!mHasRetrievedAllMedia) { + refreshMediaFromServer(0, true); + } + } else { + updateEmptyView(EmptyViewMessageType.NETWORK_ERROR); + } + } + } + + public void refreshMediaFromServer(int offset, final boolean auto) { + if (!NetworkUtils.isNetworkAvailable(getActivity())) { + updateEmptyView(EmptyViewMessageType.NETWORK_ERROR); + setRefreshing(false); + return; + } + + // do not refresh if custom date filter is shown + if (WordPress.getCurrentBlog() == null || mFilter == Filter.CUSTOM_DATE) { + setRefreshing(false); + return; + } + + // do not refresh if in search + if (mSearchTerm != null && mSearchTerm.length() > 0) { + setRefreshing(false); + return; + } + + if (offset == 0 || !mIsRefreshing) { + if (offset == mOldMediaSyncOffset) { + // we're pulling the same data again for some reason. Pull from the beginning. + offset = 0; + } + mOldMediaSyncOffset = offset; + + mIsRefreshing = true; + updateEmptyView(EmptyViewMessageType.LOADING); + mListener.onMediaItemListDownloadStart(); + mGridAdapter.setRefreshing(true); + + List<Object> apiArgs = new ArrayList<Object>(); + apiArgs.add(WordPress.getCurrentBlog()); + + Callback callback = new Callback() { + // refresh db from server. If returned count is 0, we've retrieved all the media. + // stop retrieving until the user manually refreshes + + @Override + public void onSuccess(int count) { + MediaGridAdapter adapter = (MediaGridAdapter) mGridView.getAdapter(); + mHasRetrievedAllMedia = (count == 0); + adapter.setHasRetrievedAll(mHasRetrievedAllMedia); + + mIsRefreshing = false; + + // the activity may be gone by the time this finishes, so check for it + if (getActivity() != null && MediaGridFragment.this.isVisible()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + refreshSpinnerAdapter(); + updateEmptyView(EmptyViewMessageType.NO_CONTENT); + if (!auto) { + mGridView.setSelection(0); + } + mListener.onMediaItemListDownloaded(); + mGridAdapter.setRefreshing(false); + mSwipeToRefreshHelper.setRefreshing(false); + } + }); + } + } + + @Override + public void onFailure(final ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) { + if (errorType != ApiHelper.ErrorType.NO_ERROR) { + if (getActivity() != null) { + if (errorType != ApiHelper.ErrorType.NO_UPLOAD_FILES_CAP) { + ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_media), + Duration.LONG); + } else { + if (mEmptyView == null || mEmptyView.getVisibility() != View.VISIBLE) { + ToastUtils.showToast(getActivity(), getString( + R.string.media_error_no_permission)); + } + } + } + MediaGridAdapter adapter = (MediaGridAdapter) mGridView.getAdapter(); + mHasRetrievedAllMedia = true; + adapter.setHasRetrievedAll(mHasRetrievedAllMedia); + } + + // the activity may be cone by the time we get this, so check for it + if (getActivity() != null && MediaGridFragment.this.isVisible()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + mIsRefreshing = false; + mListener.onMediaItemListDownloaded(); + mGridAdapter.setRefreshing(false); + mSwipeToRefreshHelper.setRefreshing(false); + if (errorType == ApiHelper.ErrorType.NO_UPLOAD_FILES_CAP) { + updateEmptyView(EmptyViewMessageType.PERMISSION_ERROR); + } else { + updateEmptyView(EmptyViewMessageType.GENERIC_ERROR); + } + } + }); + } + } + }; + + ApiHelper.SyncMediaLibraryTask getMediaTask = new ApiHelper.SyncMediaLibraryTask(offset, mFilter, callback); + getMediaTask.execute(apiArgs); + } + } + + public void search(String searchTerm) { + mSearchTerm = searchTerm; + Blog blog = WordPress.getCurrentBlog(); + if (blog != null) { + String blogId = String.valueOf(blog.getLocalTableBlogId()); + Cursor cursor = WordPress.wpDB.getMediaFilesForBlog(blogId, searchTerm); + mGridAdapter.changeCursor(cursor); + } + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + Cursor cursor = ((MediaGridAdapter) parent.getAdapter()).getCursor(); + String mediaId = cursor.getString(cursor.getColumnIndex("mediaId")); + mListener.onMediaItemSelected(mediaId); + } + + public void setFilterVisibility(int visibility) { + if (mSpinner != null) { + mSpinner.setVisibility(visibility); + } + } + + private void updateEmptyView(EmptyViewMessageType emptyViewMessageType) { + if (mEmptyView != null) { + if (mGridAdapter.getDataCount() == 0) { + int stringId = 0; + + switch (emptyViewMessageType) { + case LOADING: + stringId = R.string.media_fetching; + break; + case NO_CONTENT: + stringId = R.string.media_empty_list; + break; + case NETWORK_ERROR: + // Don't overwrite NO_CONTENT_CUSTOM_DATE message, since refresh is disabled with that filter on + if (mEmptyViewMessageType == EmptyViewMessageType.NO_CONTENT_CUSTOM_DATE) { + mEmptyView.setVisibility(View.VISIBLE); + return; + } + stringId = R.string.no_network_message; + break; + case PERMISSION_ERROR: + stringId = R.string.media_error_no_permission; + break; + case GENERIC_ERROR: + stringId = R.string.error_refresh_media; + break; + case NO_CONTENT_CUSTOM_DATE: + stringId = R.string.media_empty_list_custom_date; + break; + } + + mEmptyViewTitle.setText(getText(stringId)); + mEmptyViewMessageType = emptyViewMessageType; + mEmptyView.setVisibility(View.VISIBLE); + } else { + mEmptyView.setVisibility(View.GONE); + } + } + } + + private void hideEmptyView() { + if (mEmptyView != null) { + mEmptyView.setVisibility(View.GONE); + } + } + + public void setFilter(Filter filter) { + mFilter = filter; + Cursor cursor = filterItems(mFilter); + if (filter != Filter.CUSTOM_DATE || cursor == null || cursor.getCount() == 0) { + mResultView.setVisibility(View.GONE); + } + if (cursor != null && cursor.getCount() != 0) { + mGridAdapter.swapCursor(cursor); + hideEmptyView(); + } else { + // No data to display. Clear the GridView and display a message in the empty view + mGridAdapter.changeCursor(null); + } + if (filter != Filter.CUSTOM_DATE) { + // Overwrite the LOADING and NO_CONTENT_CUSTOM_DATE messages + if (mEmptyViewMessageType == EmptyViewMessageType.LOADING || + mEmptyViewMessageType == EmptyViewMessageType.NO_CONTENT_CUSTOM_DATE) { + updateEmptyView(EmptyViewMessageType.NO_CONTENT); + } else { + updateEmptyView(mEmptyViewMessageType); + } + } else { + updateEmptyView(EmptyViewMessageType.NO_CONTENT_CUSTOM_DATE); + } + } + + Cursor setDateFilter() { + Blog blog = WordPress.getCurrentBlog(); + + if (blog == null) + return null; + + String blogId = String.valueOf(blog.getLocalTableBlogId()); + + GregorianCalendar startDate = new GregorianCalendar(mStartYear, mStartMonth, mStartDay); + GregorianCalendar endDate = new GregorianCalendar(mEndYear, mEndMonth, mEndDay); + + long one_day = 24 * 60 * 60 * 1000; + Cursor cursor = WordPress.wpDB.getMediaFilesForBlog(blogId, startDate.getTimeInMillis(), endDate.getTimeInMillis() + one_day); + mGridAdapter.swapCursor(cursor); + + if (cursor != null && cursor.moveToFirst()) { + mResultView.setVisibility(View.VISIBLE); + hideEmptyView(); + DateFormat format = DateFormat.getDateInstance(); + String formattedStart = format.format(startDate.getTime()); + String formattedEnd = format.format(endDate.getTime()); + mResultView.setText(String.format(getString(R.string.media_gallery_date_range), formattedStart, + formattedEnd)); + return cursor; + } else { + updateEmptyView(EmptyViewMessageType.NO_CONTENT_CUSTOM_DATE); + } + return null; + } + + public void clearSelectedItems() { + mGridAdapter.clearSelection(); + } + + private Cursor filterItems(Filter filter) { + Blog blog = WordPress.getCurrentBlog(); + + if (blog == null) + return null; + + String blogId = String.valueOf(blog.getLocalTableBlogId()); + + switch (filter) { + case ALL: + return WordPress.wpDB.getMediaFilesForBlog(blogId); + case IMAGES: + return WordPress.wpDB.getMediaImagesForBlog(blogId); + case UNATTACHED: + return WordPress.wpDB.getMediaUnattachedForBlog(blogId); + case CUSTOM_DATE: + // show date picker only when the user clicks on the spinner, not when we are doing syncing + if (mIsDateFilterSet) { + mIsDateFilterSet = false; + showDatePicker(); + } else { + return setDateFilter(); + } + break; + } + return null; + } + + void showDatePicker() { + // Inflate your custom layout containing 2 DatePickers + LayoutInflater inflater = getActivity().getLayoutInflater(); + View customView = inflater.inflate(R.layout.date_range_dialog, null); + + // Define your date pickers + final DatePicker dpStartDate = (DatePicker) customView.findViewById(R.id.dpStartDate); + final DatePicker dpEndDate = (DatePicker) customView.findViewById(R.id.dpEndDate); + + // Build the dialog + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setView(customView); // Set the view of the dialog to your custom layout + builder.setTitle("Select start and end date"); + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mStartYear = dpStartDate.getYear(); + mStartMonth = dpStartDate.getMonth(); + mStartDay = dpStartDate.getDayOfMonth(); + mEndYear = dpEndDate.getYear(); + mEndMonth = dpEndDate.getMonth(); + mEndDay = dpEndDate.getDayOfMonth(); + setDateFilter(); + + dialog.dismiss(); + } + }); + + // Create and show the dialog + mDatePickerDialog = builder.create(); + mDatePickerDialog.show(); + } + + @Override + public void fetchMoreData(int offset) { + if (!mHasRetrievedAllMedia) { + refreshMediaFromServer(offset, true); + } + } + + @Override + public void onMovedToScrapHeap(View view) { + // cancel image fetch requests if the view has been moved to recycler. + + View imageView = view.findViewById(R.id.media_grid_item_image); + if (imageView != null) { + // this tag is set in the MediaGridAdapter class + String tag = (String) imageView.getTag(); + if (tag != null && tag.startsWith("http")) { + // need a listener to cancel request, even if the listener does nothing + ImageContainer container = WordPress.imageLoader.get(tag, new ImageListener() { + @Override + public void onErrorResponse(VolleyError error) { } + + @Override + public void onResponse(ImageContainer response, boolean isImmediate) { } + + }); + container.cancelRequest(); + } + } + + CheckableFrameLayout layout = (CheckableFrameLayout) view.findViewById(R.id.media_grid_frame_layout); + if (layout != null) { + layout.setOnCheckedChangeListener(null); + } + } + + public void setFilterSpinnerVisible(boolean visible) { + if (visible) { + mSpinner.setEnabled(true); + mSpinnerContainer.setEnabled(true); + mSpinnerContainer.setVisibility(View.VISIBLE); + } else { + mSpinner.setEnabled(false); + mSpinnerContainer.setEnabled(false); + mSpinnerContainer.setVisibility(View.GONE); + } + } + + @Override + public void onRetryUpload(String mediaId) { + mListener.onRetryUpload(mediaId); + } + + public boolean hasRetrievedAllMediaFromServer() { + return mHasRetrievedAllMedia; + } + + /* + * called by activity when blog is changed + */ + protected void reset() { + mGridAdapter.clearSelection(); + mGridView.setSelection(0); + mGridView.requestFocusFromTouch(); + mGridView.setSelection(0); + mGridAdapter.setImageLoader(MediaImageLoader.getInstance()); + mGridAdapter.changeCursor(null); + resetSpinnerAdapter(); + mHasRetrievedAllMedia = false; + } + + public void removeFromMultiSelect(String mediaId) { + if (isInMultiSelect() && mGridAdapter.isItemSelected(mediaId)) { + mGridAdapter.setItemSelected(mediaId, false); + setFilterSpinnerVisible(mGridAdapter.getSelectedItems().size() == 0); + } + } + + public void setRefreshing(boolean refreshing) { + mSwipeToRefreshHelper.setRefreshing(refreshing); + } + + public void setSwipeToRefreshEnabled(boolean enabled) { + mSwipeToRefreshHelper.setEnabled(enabled); + } + + @Override + public boolean isInMultiSelect() { + return mIsMultiSelect; + } + + public class MultiChoiceModeListener implements GridView.MultiChoiceModeListener { + private MenuItem mNewPostButton; + private MenuItem mNewGalleryButton; + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + int selectCount = mGridAdapter.getSelectedItems().size(); + mode.setTitle(String.format(getString(R.string.cab_selected), selectCount)); + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.media_multiselect, menu); + mNewPostButton = menu.findItem(R.id.media_multiselect_actionbar_post); + mNewGalleryButton = menu.findItem(R.id.media_multiselect_actionbar_gallery); + setSwipeToRefreshEnabled(false); + mIsMultiSelect = true; + updateActionButtons(selectCount); + return true; + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return true; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + int i = item.getItemId(); + if (i == R.id.media_multiselect_actionbar_post) { + handleNewPost(); + return true; + } else if (i == R.id.media_multiselect_actionbar_gallery) { + handleMultiSelectPost(); + return true; + } else if (i == R.id.media_multiselect_actionbar_trash) { + handleMultiSelectDelete(); + return true; + } + return true; + } + + public void onDestroyActionMode(ActionMode mode) { + mGridAdapter.clearSelection(); + setSwipeToRefreshEnabled(true); + mIsMultiSelect = false; + setFilterSpinnerVisible(mGridAdapter.getSelectedItems().size() == 0); + } + + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + mGridAdapter.setItemSelected(position, checked); + int selectCount = mGridAdapter.getSelectedItems().size(); + setFilterSpinnerVisible(selectCount == 0); + mode.setTitle(String.format(getString(R.string.cab_selected), selectCount)); + updateActionButtons(selectCount); + } + + private void updateActionButtons(int selectCount) { + switch (selectCount) { + case 1: + mNewPostButton.setVisible(true); + mNewGalleryButton.setVisible(false); + break; + default: + mNewPostButton.setVisible(false); + mNewGalleryButton.setVisible(true); + break; + } + } + + private void handleNewPost() { + if (!isAdded()) { + return; + } + ArrayList<String> ids = mGridAdapter.getSelectedItems(); + Intent i = new Intent(getActivity(), EditPostActivity.class); + i.setAction(EditPostActivity.NEW_MEDIA_POST); + i.putExtra(EditPostActivity.NEW_MEDIA_POST_EXTRA, ids.iterator().next()); + startActivity(i); + } + + private void handleMultiSelectDelete() { + if (!isAdded()) { + return; + } + Builder builder = new AlertDialog.Builder(getActivity()).setMessage(R.string.confirm_delete_multi_media) + .setCancelable(true).setPositiveButton( + R.string.delete, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (getActivity() instanceof MediaBrowserActivity) { + ((MediaBrowserActivity) getActivity()).deleteMedia( + mGridAdapter.getSelectedItems()); + } + refreshSpinnerAdapter(); + } + }).setNegativeButton(R.string.cancel, null); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + private void handleMultiSelectPost() { + if (!isAdded()) { + return; + } + Intent i = new Intent(getActivity(), EditPostActivity.class); + i.setAction(EditPostActivity.NEW_MEDIA_GALLERY); + i.putStringArrayListExtra(EditPostActivity.NEW_MEDIA_GALLERY_EXTRA_IDS, + mGridAdapter.getSelectedItems()); + startActivity(i); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaImageLoader.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaImageLoader.java new file mode 100644 index 000000000..d484d2fa5 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaImageLoader.java @@ -0,0 +1,42 @@ +package org.wordpress.android.ui.media; + +import android.content.Context; + +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.Volley; + +import org.wordpress.android.WordPress; +import org.wordpress.android.models.Blog; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.VolleyUtils; + +/** + * provides the ImageLoader and backing RequestQueue for media image requests - necessary because + * images in protected blogs need to be authenticated, which requires a separate RequestQueue + */ +class MediaImageLoader { + private MediaImageLoader() { + throw new AssertionError(); + } + + static ImageLoader getInstance() { + return getInstance(WordPress.getCurrentBlog()); + } + + static ImageLoader getInstance(Blog blog) { + if (blog != null && VolleyUtils.isCustomHTTPClientStackNeeded(blog)) { + // use ImageLoader with authenticating request queue for protected blogs + AppLog.d(AppLog.T.MEDIA, "using custom imageLoader"); + Context context = WordPress.getContext(); + RequestQueue authRequestQueue = Volley.newRequestQueue(context, VolleyUtils.getHTTPClientStack(context, blog)); + ImageLoader imageLoader = new ImageLoader(authRequestQueue, WordPress.getBitmapCache()); + imageLoader.setBatchedResponseDelay(0); + return imageLoader; + } else { + // use default ImageLoader for all others + AppLog.d(AppLog.T.MEDIA, "using default imageLoader"); + return WordPress.imageLoader; + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaItemFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaItemFragment.java new file mode 100644 index 000000000..53f5be0eb --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaItemFragment.java @@ -0,0 +1,380 @@ +package org.wordpress.android.ui.media; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.Fragment; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.WordPressDB; +import org.wordpress.android.models.Blog; +import org.wordpress.android.ui.reader.ReaderActivityLauncher; +import org.wordpress.android.ui.reader.ReaderActivityLauncher.PhotoViewerOption; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.DisplayUtils; +import org.wordpress.android.util.ImageUtils.BitmapWorkerCallback; +import org.wordpress.android.util.ImageUtils.BitmapWorkerTask; +import org.wordpress.android.util.MediaUtils; +import org.wordpress.android.util.SqlUtils; +import org.wordpress.android.util.StringUtils; +import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.UrlUtils; +import org.wordpress.android.widgets.WPNetworkImageView; + +import java.util.ArrayList; +import java.util.EnumSet; + +/** + * A fragment display a media item's details. + */ +public class MediaItemFragment extends Fragment { + private static final String ARGS_MEDIA_ID = "media_id"; + + public static final String TAG = MediaItemFragment.class.getName(); + + private WPNetworkImageView mImageView; + private TextView mCaptionView; + private TextView mDescriptionView; + private TextView mDateView; + private TextView mFileNameView; + private TextView mFileTypeView; + private MediaItemFragmentCallback mCallback; + + private boolean mIsLocal; + private String mImageUri; + + public interface MediaItemFragmentCallback { + void onResume(Fragment fragment); + void onPause(Fragment fragment); + } + + public static MediaItemFragment newInstance(String mediaId) { + MediaItemFragment fragment = new MediaItemFragment(); + + Bundle args = new Bundle(); + args.putString(ARGS_MEDIA_ID, mediaId); + fragment.setArguments(args); + + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + mCallback = (MediaItemFragmentCallback) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + " must implement MediaItemFragmentCallback"); + } + } + + @Override + public void onResume() { + super.onResume(); + mCallback.onResume(this); + loadMedia(getMediaId()); + } + + @Override + public void onPause() { + super.onPause(); + mCallback.onPause(this); + } + + public String getMediaId() { + if (getArguments() != null) { + return getArguments().getString(ARGS_MEDIA_ID); + } else { + return null; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.media_listitem_details, container, false); + + mCaptionView = (TextView) view.findViewById(R.id.media_listitem_details_caption); + mDescriptionView = (TextView) view.findViewById(R.id.media_listitem_details_description); + mDateView = (TextView) view.findViewById(R.id.media_listitem_details_date); + mFileNameView = (TextView) view.findViewById(R.id.media_listitem_details_file_name); + mFileTypeView = (TextView) view.findViewById(R.id.media_listitem_details_file_type); + mImageView = (WPNetworkImageView) view.findViewById(R.id.media_listitem_details_image); + + return view; + } + + /** Loads the first media item for the current blog from the database **/ + public void loadDefaultMedia() { + loadMedia(null); + } + + public void loadMedia(String mediaId) { + Blog blog = WordPress.getCurrentBlog(); + + if (blog != null) { + String blogId = String.valueOf(blog.getLocalTableBlogId()); + + Cursor cursor = null; + try { + // if the id is null, get the first media item in the database + if (mediaId == null) { + cursor = WordPress.wpDB.getFirstMediaFileForBlog(blogId); + } else { + cursor = WordPress.wpDB.getMediaFile(blogId, mediaId); + } + refreshViews(cursor); + } finally { + SqlUtils.closeCursor(cursor); + } + } + } + + private void refreshViews(Cursor cursor) { + if (!isAdded() || !cursor.moveToFirst()) { + return; + } + + // check whether or not to show the edit button + String state = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_UPLOAD_STATE)); + mIsLocal = MediaUtils.isLocalFile(state); + if (mIsLocal && getActivity() != null) { + getActivity().invalidateOptionsMenu(); + } + + String caption = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_CAPTION)); + if (TextUtils.isEmpty(caption)) { + mCaptionView.setVisibility(View.GONE); + } else { + mCaptionView.setText(caption); + mCaptionView.setVisibility(View.VISIBLE); + } + + String desc = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DESCRIPTION)); + if (TextUtils.isEmpty(desc)) { + mDescriptionView.setVisibility(View.GONE); + } else { + mDescriptionView.setText(desc); + mDescriptionView.setVisibility(View.VISIBLE); + } + + String date = MediaUtils.getDate(cursor.getLong(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_DATE_CREATED_GMT))); + mDateView.setText(date); + TextView txtDateLabel = (TextView) getView().findViewById(R.id.media_listitem_details_date_label); + txtDateLabel.setText( + mIsLocal ? R.string.media_details_label_date_added : R.string.media_details_label_date_uploaded); + + String fileURL = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL)); + String fileName = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME)); + mImageUri = TextUtils.isEmpty(fileURL) + ? cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH)) + : fileURL; + boolean isValidImage = MediaUtils.isValidImage(mImageUri); + + mFileNameView.setText(fileName); + + float mediaWidth = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_WIDTH)); + float mediaHeight = cursor.getInt(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_HEIGHT)); + + // image and dimensions + if (isValidImage) { + int screenWidth = DisplayUtils.getDisplayPixelWidth(getActivity()); + int screenHeight = DisplayUtils.getDisplayPixelHeight(getActivity()); + + // determine size for display + int imageWidth; + int imageHeight; + boolean isFullWidth; + if (mediaWidth == 0 || mediaHeight == 0) { + imageWidth = screenWidth; + imageHeight = screenHeight / 2; + isFullWidth = true; + } else if (mediaWidth > mediaHeight) { + float ratio = mediaHeight / mediaWidth; + imageWidth = Math.min(screenWidth, (int) mediaWidth); + imageHeight = (int) (imageWidth * ratio); + isFullWidth = (imageWidth == screenWidth); + } else { + float ratio = mediaWidth / mediaHeight; + imageHeight = Math.min(screenHeight / 2, (int) mediaHeight); + imageWidth = (int) (imageHeight * ratio); + isFullWidth = false; + } + + // set the imageView's parent height to match the image so it takes up space while + // the image is loading + FrameLayout frameView = (FrameLayout) getView().findViewById(R.id.layout_image_frame); + frameView.setLayoutParams( + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, imageHeight)); + + // add padding to the frame if the image isn't full-width + if (!isFullWidth) { + int hpadding = getResources().getDimensionPixelSize(R.dimen.content_margin); + int vpadding = getResources().getDimensionPixelSize(R.dimen.margin_extra_large); + frameView.setPadding(hpadding, vpadding, hpadding, vpadding); + } + + if (mIsLocal) { + final String filePath = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH)); + loadLocalImage(mImageView, filePath, imageWidth, imageHeight); + } else { + // Allow non-private wp.com and Jetpack blogs to use photon to get a higher res thumbnail + String thumbnailURL; + if (WordPress.getCurrentBlog() != null && WordPress.getCurrentBlog().isPhotonCapable()){ + thumbnailURL = StringUtils.getPhotonUrl(mImageUri, imageWidth); + } else { + thumbnailURL = UrlUtils.removeQuery(mImageUri) + "?w=" + imageWidth; + } + mImageView.setImageUrl(thumbnailURL, WPNetworkImageView.ImageType.PHOTO); + } + } else { + // not an image so show placeholder icon + int placeholderResId = WordPressMediaUtils.getPlaceholder(mImageUri); + mImageView.setDefaultImageResId(placeholderResId); + mImageView.showDefaultImage(); + } + + // show dimens & file ext together + String dimens = + (mediaWidth > 0 && mediaHeight > 0) ? (int) mediaWidth + " x " + (int) mediaHeight : null; + String fileExt = + TextUtils.isEmpty(fileURL) ? null : fileURL.replaceAll(".*\\.(\\w+)$", "$1").toUpperCase(); + boolean hasDimens = !TextUtils.isEmpty(dimens); + boolean hasExt = !TextUtils.isEmpty(fileExt); + if (hasDimens & hasExt) { + mFileTypeView.setText(fileExt + ", " + dimens); + mFileTypeView.setVisibility(View.VISIBLE); + } else if (hasExt) { + mFileTypeView.setText(fileExt); + mFileTypeView.setVisibility(View.VISIBLE); + } else { + mFileTypeView.setVisibility(View.GONE); + } + + // enable fullscreen photo for non-local + if (!mIsLocal && isValidImage) { + mImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Blog blog = WordPress.getCurrentBlog(); + boolean isPrivate = blog != null && blog.isPrivate(); + EnumSet<PhotoViewerOption> imageOptions = EnumSet.noneOf(PhotoViewerOption.class); + if (isPrivate) { + imageOptions.add(PhotoViewerOption.IS_PRIVATE_IMAGE); + } + ReaderActivityLauncher.showReaderPhotoViewer( + v.getContext(), mImageUri, imageOptions); + } + }); + } + } + + private synchronized void loadLocalImage(ImageView imageView, String filePath, int width, int height) { + if (MediaUtils.isValidImage(filePath)) { + imageView.setTag(filePath); + + Bitmap bitmap = WordPress.getBitmapCache().get(filePath); + if (bitmap != null) { + imageView.setImageBitmap(bitmap); + } else { + BitmapWorkerTask task = new BitmapWorkerTask(imageView, width, height, new BitmapWorkerCallback() { + @Override + public void onBitmapReady(String path, ImageView imageView, Bitmap bitmap) { + imageView.setImageBitmap(bitmap); + WordPress.getBitmapCache().put(path, bitmap); + } + }); + task.execute(filePath); + } + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.media_details, menu); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.menu_new_media).setVisible(false); + menu.findItem(R.id.menu_search).setVisible(false); + + menu.findItem(R.id.menu_edit_media).setVisible( + !mIsLocal && WordPressMediaUtils.isWordPressVersionWithMediaEditingCapabilities()); + + menu.findItem(R.id.menu_copy_media_url).setVisible(!mIsLocal && !TextUtils.isEmpty(mImageUri)); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + + if (itemId == R.id.menu_delete) { + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + boolean canDeleteMedia = WordPressMediaUtils.canDeleteMedia(blogId, getMediaId()); + if (!canDeleteMedia) { + Toast.makeText(getActivity(), R.string.wait_until_upload_completes, Toast.LENGTH_LONG).show(); + return true; + } + + Builder builder = new AlertDialog.Builder(getActivity()).setMessage(R.string.confirm_delete_media) + .setCancelable(true).setPositiveButton( + R.string.delete, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ArrayList<String> ids = new ArrayList<>(1); + ids.add(getMediaId()); + if (getActivity() instanceof MediaBrowserActivity) { + ((MediaBrowserActivity) getActivity()).deleteMedia(ids); + } + } + }).setNegativeButton(R.string.cancel, null); + AlertDialog dialog = builder.create(); + dialog.show(); + return true; + } else if (itemId == R.id.menu_copy_media_url) { + copyUrlToClipboard(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + private void copyUrlToClipboard() { + try { + ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText(mImageUri, mImageUri)); + ToastUtils.showToast(getActivity(), R.string.media_details_copy_url_toast); + } catch (Exception e) { + AppLog.e(AppLog.T.UTILS, e); + ToastUtils.showToast(getActivity(), R.string.error_copy_to_clipboard); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPickerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPickerActivity.java new file mode 100644 index 000000000..ea4664f67 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPickerActivity.java @@ -0,0 +1,538 @@ +package org.wordpress.android.ui.media; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.design.widget.TabLayout; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.Surface; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.TranslateAnimation; +import android.widget.LinearLayout; + +import com.android.volley.toolbox.ImageLoader; + +import org.wordpress.android.BuildConfig; +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.ui.RequestCodes; +import org.wordpress.android.util.MediaUtils; +import org.wordpress.android.widgets.WPViewPager; +import org.wordpress.mediapicker.MediaItem; +import org.wordpress.mediapicker.MediaPickerFragment; +import org.wordpress.mediapicker.source.MediaSource; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Allows users to select a variety of videos and images from any source. + * + * Title can be set either by defining a string resource, R.string.media_picker_title, or passing + * a String extra in the {@link android.content.Intent} with the key ACTIVITY_TITLE_KEY. + * + * Accepts Image and Video sources as arguments and displays each in a tab. + * - Use DEVICE_IMAGE_MEDIA_SOURCES_KEY with a {@link java.util.List} of {@link org.wordpress.mediapicker.source.MediaSource}'s to pass image sources via the Intent + * - Use DEVICE_VIDEO_MEDIA_SOURCES_KEY with a {@link java.util.List} of {@link org.wordpress.mediapicker.source.MediaSource}'s to pass video sources via the Intent + */ + +public class MediaPickerActivity extends AppCompatActivity + implements MediaPickerFragment.OnMediaSelected { + /** + * Request code for the {@link android.content.Intent} to start media selection. + */ + public static final int ACTIVITY_REQUEST_CODE_MEDIA_SELECTION = 6000; + /** + * Result code signaling that media has been selected. + */ + public static final int ACTIVITY_RESULT_CODE_MEDIA_SELECTED = 6001; + /** + * Result code signaling that a gallery should be created with the results. + */ + public static final int ACTIVITY_RESULT_CODE_GALLERY_CREATED = 6002; + + /** + * Pass a {@link String} with this key in the {@link android.content.Intent} to set the title. + */ + public static final String ACTIVITY_TITLE_KEY = "activity-title"; + /** + * Pass an {@link java.util.ArrayList} of {@link org.wordpress.mediapicker.source.MediaSource}'s + * in the {@link android.content.Intent} to set image sources for selection. + */ + public static final String DEVICE_IMAGE_MEDIA_SOURCES_KEY = "device-image-media-sources"; + /** + * Pass an {@link java.util.ArrayList} of {@link org.wordpress.mediapicker.source.MediaSource}'s + * in the {@link android.content.Intent} to set video sources for selection. + */ + public static final String DEVICE_VIDEO_MEDIA_SOURCES_KEY = "device- video=media-sources"; + public static final String BLOG_IMAGE_MEDIA_SOURCES_KEY = "blog-image-media-sources"; + public static final String BLOG_VIDEO_MEDIA_SOURCES_KEY = "blog-video-media-sources"; + /** + * Key to extract the {@link java.util.ArrayList} of {@link org.wordpress.mediapicker.MediaItem}'s + * that were selected by the user. + */ + public static final String SELECTED_CONTENT_RESULTS_KEY = "selected-content"; + + private static final String CAPTURE_PATH_KEY = "capture-path"; + + private static final long TAB_ANIMATION_DURATION_MS = 250L; + + private MediaPickerAdapter mMediaPickerAdapter; + private ArrayList<MediaSource>[] mMediaSources; + private TabLayout mTabLayout; + private WPViewPager mViewPager; + private ActionMode mActionMode; + private String mCapturePath; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + lockRotation(); + addMediaSources(); + setTitle(); + initializeContentView(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.media_picker, menu); + + return true; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString(CAPTURE_PATH_KEY, mCapturePath); + } + + @Override + public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + if (savedInstanceState.containsKey(CAPTURE_PATH_KEY)) { + mCapturePath = savedInstanceState.getString(CAPTURE_PATH_KEY); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + } else if (item.getItemId() == R.id.capture_image) { + WordPressMediaUtils.launchCamera(this, BuildConfig.APPLICATION_ID, + new WordPressMediaUtils.LaunchCameraCallback() { + @Override + public void onMediaCapturePathReady(String mediaCapturePath) { + mCapturePath = mediaCapturePath; + } + }); + return true; + } else if (item.getItemId() == R.id.capture_video) { + WordPressMediaUtils.launchVideoCamera(this); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + switch (requestCode) { + case RequestCodes.TAKE_PHOTO: + File file = new File(mCapturePath); + Uri imageUri = Uri.fromFile(file); + + if (file.exists() && MediaUtils.isValidImage(imageUri.toString())) { + // Notify MediaStore of new content + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, imageUri)); + + MediaItem newImage = new MediaItem(); + newImage.setSource(imageUri); + newImage.setPreviewSource(imageUri); + ArrayList<MediaItem> imageResult = new ArrayList<>(); + imageResult.add(newImage); + finishWithResults(imageResult, ACTIVITY_RESULT_CODE_MEDIA_SELECTED); + } + break; + case RequestCodes.TAKE_VIDEO: + Uri videoUri = data != null ? data.getData() : null; + + if (videoUri != null) { + // Notify MediaStore of new content + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, videoUri)); + + MediaItem newVideo = new MediaItem(); + newVideo.setSource(videoUri); + newVideo.setPreviewSource(videoUri); + ArrayList<MediaItem> videoResult = new ArrayList<>(); + videoResult.add(newVideo); + finishWithResults(videoResult, ACTIVITY_RESULT_CODE_MEDIA_SELECTED); + } + break; + default: + break; + } + } + + @Override + public void onBackPressed() { + if (mActionMode != null) { + mActionMode.finish(); + } else { + finishWithResults(null, ACTIVITY_RESULT_CODE_MEDIA_SELECTED); + super.onBackPressed(); + } + } + + @Override + public void onActionModeStarted(ActionMode mode) { + super.onActionModeStarted(mode); + + mViewPager.setPagingEnabled(false); + mActionMode = mode; + + animateTabGone(); + } + + @Override + public void onActionModeFinished(ActionMode mode) { + super.onActionModeFinished(mode); + + mViewPager.setPagingEnabled(true); + mActionMode = null; + + animateTabAppear(); + } + + /* + OnMediaSelected interface + */ + + @Override + public void onMediaSelectionStarted() { + } + + @Override + public void onMediaSelected(MediaItem mediaContent, boolean selected) { + } + + @Override + public void onMediaSelectionConfirmed(ArrayList<MediaItem> mediaContent) { + if (mediaContent != null) { + finishWithResults(mediaContent, ACTIVITY_RESULT_CODE_MEDIA_SELECTED); + } else { + finish(); + } + } + + @Override + public void onMediaSelectionCancelled() { + } + + @Override + public boolean onMenuItemSelected(MenuItem menuItem, ArrayList<MediaItem> selectedContent) { + if (menuItem.getItemId() == R.id.menu_media_content_selection_gallery) { + finishWithResults(selectedContent, ACTIVITY_RESULT_CODE_GALLERY_CREATED); + } + + return false; + } + + @Override + public ImageLoader.ImageCache getImageCache() { + return WordPress.getBitmapCache(); + } + + /** + * Finishes the activity after the user has confirmed media selection. + * + * @param results + * list of selected media items + */ + private void finishWithResults(ArrayList<MediaItem> results, int resultCode) { + Intent result = new Intent(); + result.putParcelableArrayListExtra(SELECTED_CONTENT_RESULTS_KEY, results); + setResult(resultCode, result); + finish(); + } + + /** + * Helper method; sets title to R.string.media_picker_title unless intent defines one + */ + private void setTitle() { + final Intent intent = getIntent(); + + if (intent != null && intent.hasExtra(ACTIVITY_TITLE_KEY)) { + String activityTitle = intent.getStringExtra(ACTIVITY_TITLE_KEY); + setTitle(activityTitle); + } else { + setTitle(getString(R.string.media_picker_title)); + } + } + + /** + * Helper method; gathers {@link org.wordpress.mediapicker.source.MediaSource}'s from intent + */ + private void addMediaSources() { + final Intent intent = getIntent(); + + if (intent != null) { + mMediaSources = new ArrayList[4]; + + List<MediaSource> mediaSources = intent.getParcelableArrayListExtra(DEVICE_IMAGE_MEDIA_SOURCES_KEY); + if (mediaSources != null) { + mMediaSources[0] = new ArrayList<>(); + mMediaSources[0].addAll(mediaSources); + } + + mediaSources = intent.getParcelableArrayListExtra(DEVICE_VIDEO_MEDIA_SOURCES_KEY); + if (mediaSources != null) { + mMediaSources[1] = new ArrayList<>(); + mMediaSources[1].addAll(mediaSources); + } + + mediaSources = intent.getParcelableArrayListExtra(BLOG_IMAGE_MEDIA_SOURCES_KEY); + if (mediaSources != null) { + mMediaSources[2] = new ArrayList<>(); + mMediaSources[2].addAll(mediaSources); + } + + mediaSources = intent.getParcelableArrayListExtra(BLOG_VIDEO_MEDIA_SOURCES_KEY); + if (mediaSources != null) { + mMediaSources[3] = new ArrayList<>(); + mMediaSources[3].addAll(mediaSources); + } + } + } + + /** + * Helper method; locks device orientation to its current state while media is being selected + */ + private void lockRotation() { + switch (getWindowManager().getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + break; + case Surface.ROTATION_90: + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + break; + case Surface.ROTATION_180: + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT); + break; + case Surface.ROTATION_270: + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE); + break; + } + } + + /** + * Helper method; sets up the tab bar, media adapter, and ViewPager for displaying media content + */ + private void initializeContentView() { + setContentView(R.layout.media_picker_activity); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.show(); + } + + mMediaPickerAdapter = new MediaPickerAdapter(getFragmentManager()); + mTabLayout = (TabLayout) findViewById(R.id.tab_layout); + mViewPager = (WPViewPager) findViewById(R.id.media_picker_pager); + + if (mViewPager != null) { + mViewPager.setPagingEnabled(true); + + mMediaPickerAdapter.addTab(mMediaSources[0] != null ? mMediaSources[0] : + new ArrayList<MediaSource>(), + getString(R.string.tab_title_device_images), + getString(R.string.loading_images), + getString(R.string.error_loading_images), + getString(R.string.no_device_images)); + mMediaPickerAdapter.addTab(mMediaSources[1] != null ? mMediaSources[1] : + new ArrayList<MediaSource>(), + getString(R.string.tab_title_device_videos), + getString(R.string.loading_videos), + getString(R.string.error_loading_videos), + getString(R.string.no_device_videos)); + mMediaPickerAdapter.addTab(mMediaSources[2] != null ? mMediaSources[2] : + new ArrayList<MediaSource>(), + getString(R.string.tab_title_site_images), + getString(R.string.loading_blog_images), + getString(R.string.error_loading_blog_images), + getString(R.string.no_blog_images)); + mMediaPickerAdapter.addTab(mMediaSources[3] != null ? mMediaSources[3] : + new ArrayList<MediaSource>(), + getString(R.string.tab_title_site_videos), + getString(R.string.loading_blog_videos), + getString(R.string.error_loading_blog_videos), + getString(R.string.no_blog_videos)); + + mViewPager.setAdapter(mMediaPickerAdapter); + + if (mTabLayout != null) { + int normalColor = getResources().getColor(R.color.blue_light); + int selectedColor = getResources().getColor(R.color.white); + mTabLayout.setTabTextColors(normalColor, selectedColor); + mTabLayout.setTabMode(TabLayout.MODE_SCROLLABLE); + mTabLayout.setupWithViewPager(mViewPager); + } + } + } + + /** + * Helper method; animates the tab bar and ViewPager in when ActionMode ends + */ + private void animateTabAppear() { + TranslateAnimation tabAppearAnimation = new TranslateAnimation(0, 0, -mTabLayout.getHeight(), 0); + TranslateAnimation pagerAppearAnimation = new TranslateAnimation(0, 0, -mTabLayout.getHeight(), 0); + + tabAppearAnimation.setDuration(TAB_ANIMATION_DURATION_MS); + pagerAppearAnimation.setDuration(TAB_ANIMATION_DURATION_MS); + pagerAppearAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mViewPager.getWidth(), mViewPager.getHeight() - mTabLayout.getHeight()); + mViewPager.setLayoutParams(params); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + + mTabLayout.setVisibility(View.VISIBLE); + mViewPager.startAnimation(pagerAppearAnimation); + mTabLayout.startAnimation(tabAppearAnimation); + } + + /** + * Helper method; animates the tab bar and ViewPager out when ActionMode begins + */ + private void animateTabGone() { + TranslateAnimation tabGoneAnimation = new TranslateAnimation(0, 0, 0, -mTabLayout.getHeight()); + TranslateAnimation pagerGoneAnimation = new TranslateAnimation(0, 0, 0, -mTabLayout.getHeight()); + tabGoneAnimation.setDuration(TAB_ANIMATION_DURATION_MS); + pagerGoneAnimation.setDuration(TAB_ANIMATION_DURATION_MS); + tabGoneAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mTabLayout.setVisibility(View.GONE); + mTabLayout.clearAnimation(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + pagerGoneAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + LinearLayout.LayoutParams newParams = new LinearLayout.LayoutParams(mViewPager.getWidth(), mViewPager.getHeight() + mTabLayout.getHeight()); + mViewPager.setLayoutParams(newParams); + } + + @Override + public void onAnimationEnd(Animation animation) { + mViewPager.clearAnimation(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + + mTabLayout.startAnimation(tabGoneAnimation); + mViewPager.startAnimation(pagerGoneAnimation); + } + + /** + * Shows {@link org.wordpress.mediapicker.MediaPickerFragment}'s in a tabbed layout. + */ + public class MediaPickerAdapter extends FragmentPagerAdapter { + private class MediaPicker { + public String loadingText; + public String errorText; + public String emptyText; + public String pickerTitle; + public ArrayList<MediaSource> mediaSources; + + public MediaPicker(String name, String loading, String error, String empty, ArrayList<MediaSource> sources) { + loadingText = loading; + errorText = error; + emptyText = empty; + pickerTitle = name; + mediaSources = sources; + } + } + + private final List<MediaPicker> mMediaPickers; + + private MediaPickerAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + + mMediaPickers = new ArrayList<>(); + } + + @Override + public Fragment getItem(int position) { + if (position < mMediaPickers.size()) { + MediaPicker mediaPicker = mMediaPickers.get(position); + MediaPickerFragment fragment = new MediaPickerFragment(); + fragment.setLoadingText(mediaPicker.loadingText); + fragment.setErrorText(mediaPicker.errorText); + fragment.setEmptyText(mediaPicker.emptyText); + fragment.setActionModeMenu(R.menu.menu_media_picker_action_mode); + fragment.setMediaSources(mediaPicker.mediaSources); + + return fragment; + } + + return null; + } + + @Override + public int getCount() { + return mMediaPickers.size(); + } + + @Override + public CharSequence getPageTitle(int position) { + return mMediaPickers.get(position).pickerTitle; + } + + public void addTab(ArrayList<MediaSource> mediaSources, String tabName, String loading, String error, String empty) { + mMediaPickers.add(new MediaPicker(tabName, loading, error, empty, mediaSources)); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPImages.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPImages.java new file mode 100644 index 000000000..26e61ae12 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPImages.java @@ -0,0 +1,264 @@ +package org.wordpress.android.ui.media; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Parcel; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.android.volley.toolbox.ImageLoader; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.WordPressDB; +import org.wordpress.android.models.Blog; +import org.wordpress.mediapicker.MediaItem; +import org.wordpress.mediapicker.source.MediaSource; +import org.wordpress.mediapicker.MediaUtils.LimitedBackgroundOperation; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +public class MediaSourceWPImages implements MediaSource { + private final List<MediaItem> mVerifiedItems = new ArrayList<>(); + private final List<MediaItem> mMediaItems = new ArrayList<>(); + + private OnMediaChange mListener; + + public MediaSourceWPImages() { + } + + @Override + public void gather(Context context) { + mMediaItems.clear(); + + Blog blog = WordPress.getCurrentBlog(); + + if (blog != null) { + Cursor imageCursor = WordPressMediaUtils.getWordPressMediaImages(String.valueOf(blog.getLocalTableBlogId())); + + if (imageCursor != null) { + addWordPressImagesFromCursor(imageCursor); + imageCursor.close(); + } else if (mListener != null){ + mListener.onMediaLoaded(false); + } + } else if (mListener != null){ + mListener.onMediaLoaded(false); + } + } + + @Override + public void cleanup() { + mMediaItems.clear(); + } + + @Override + public void setListener(OnMediaChange listener) { + mListener = listener; + } + + @Override + public int getCount() { + return mVerifiedItems.size(); + } + + @Override + public MediaItem getMedia(int position) { + return mVerifiedItems.get(position); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, ImageLoader.ImageCache cache) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.media_item_wp_image, parent, false); + } + + if (convertView != null) { + MediaItem mediaItem = mVerifiedItems.get(position); + Uri imageSource = mediaItem.getPreviewSource(); + ImageView imageView = (ImageView) convertView.findViewById(R.id.wp_image_view_background); + if (imageView != null) { + if (imageSource != null) { + Bitmap imageBitmap = null; + if (cache != null) { + imageBitmap = cache.getBitmap(imageSource.toString()); + } + + if (imageBitmap == null) { + imageView.setImageDrawable(placeholderDrawable(convertView.getContext())); + WordPressMediaUtils.BackgroundDownloadWebImage bgDownload = + new WordPressMediaUtils.BackgroundDownloadWebImage(imageView); + imageView.setTag(bgDownload); + bgDownload.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, mediaItem.getPreviewSource()); + } else { + imageView.setImageBitmap(imageBitmap); + } + } else { + imageView.setTag(null); + imageView.setImageResource(R.color.grey_darken_10); + } + } + } + + return convertView; + } + + @Override + public boolean onMediaItemSelected(MediaItem mediaItem, boolean selected) { + return !selected; + } + + private Drawable placeholderDrawable(Context context) { + if (context != null && context.getResources() != null) { + return context.getResources().getDrawable(R.drawable.media_item_placeholder); + } + + return null; + } + + private void addWordPressImagesFromCursor(Cursor cursor) { + if (cursor.moveToFirst()) { + do { + int attachmentIdColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID); + int fileUrlColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL); + int filePathColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH); + int thumbnailColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_THUMBNAIL_URL); + + String id = ""; + if (attachmentIdColumnIndex != -1) { + id = String.valueOf(cursor.getInt(attachmentIdColumnIndex)); + } + MediaItem newContent = new MediaItem(); + newContent.setTag(id); + newContent.setTitle(""); + + if (fileUrlColumnIndex != -1) { + String fileUrl = cursor.getString(fileUrlColumnIndex); + + if (fileUrl != null) { + newContent.setSource(Uri.parse(fileUrl)); + newContent.setPreviewSource(Uri.parse(fileUrl)); + } else if (filePathColumnIndex != -1) { + String filePath = cursor.getString(filePathColumnIndex); + + if (filePath != null) { + newContent.setSource(Uri.parse(filePath)); + newContent.setPreviewSource(Uri.parse(filePath)); + } + } + } + + if (thumbnailColumnIndex != -1) { + String preview = cursor.getString(thumbnailColumnIndex); + + if (preview != null) { + newContent.setPreviewSource(Uri.parse(preview)); + } + } + + mMediaItems.add(newContent); + } while (cursor.moveToNext()); + + removeDeletedEntries(); + } else if (mListener != null) { + mListener.onMediaLoaded(true); + } + } + + private void removeDeletedEntries() { + final List<MediaItem> existingItems = new ArrayList<>(mMediaItems); + final List<MediaItem> failedItems = new ArrayList<>(); + + for (final MediaItem mediaItem : existingItems) { + LimitedBackgroundOperation<MediaItem, Void, MediaItem> backgroundCheck = + new LimitedBackgroundOperation<MediaItem, Void, MediaItem>() { + private int responseCode; + + @Override + protected MediaItem performBackgroundOperation(MediaItem[] params) { + MediaItem mediaItem = params[0]; + try { + URL mediaUrl = new URL(mediaItem.getSource().toString()); + HttpURLConnection connection = (HttpURLConnection) mediaUrl.openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + responseCode = connection.getResponseCode(); + } catch (IOException ioException) { + Log.e("", "Error reading from " + mediaItem.getSource() + "\nexception:" + ioException); + + return null; + } + + return mediaItem; + } + + @Override + public void performPostExecute(MediaItem result) { + if (mListener != null && result != null) { + if (responseCode == 200) { + mVerifiedItems.add(result); + List<MediaItem> resultList = new ArrayList<>(); + resultList.add(result); + + // Only signal newly loaded data every 3 images + if ((existingItems.size() - mVerifiedItems.size()) % 3 == 0) { + mListener.onMediaAdded(MediaSourceWPImages.this, resultList); + } + } else { + failedItems.add(result); + } + + // Notify of all media loaded if all have been processed + if ((failedItems.size() + mVerifiedItems.size()) == existingItems.size()) { + mListener.onMediaLoaded(true); + } + } + } + + @Override + public void startExecution(Object params) { + if (!(params instanceof MediaItem)) { + throw new IllegalArgumentException("Params must be of type MediaItem"); + } + executeOnExecutor(THREAD_POOL_EXECUTOR, (MediaItem) params); + } + }; + backgroundCheck.executeWithLimit(mediaItem); + } + } + + /** + * {@link android.os.Parcelable} interface + */ + + public static final Creator<MediaSourceWPImages> CREATOR = + new Creator<MediaSourceWPImages>() { + public MediaSourceWPImages createFromParcel(Parcel in) { + return new MediaSourceWPImages(); + } + + public MediaSourceWPImages[] newArray(int size) { + return new MediaSourceWPImages[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPVideos.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPVideos.java new file mode 100644 index 000000000..d87dad566 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSourceWPVideos.java @@ -0,0 +1,209 @@ +package org.wordpress.android.ui.media; + +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Parcel; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.android.volley.toolbox.ImageLoader; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.WordPressDB; +import org.wordpress.android.models.Blog; +import org.wordpress.android.util.MediaUtils; +import org.wordpress.mediapicker.MediaItem; +import org.wordpress.mediapicker.source.MediaSource; + +import java.util.ArrayList; +import java.util.List; + +public class MediaSourceWPVideos implements MediaSource { + private static final String VIDEO_PRESS_HOST = "https://videos.files.wordpress.com/"; + private static final String VIDEO_PRESS_THUMBNAIL_APPEND = "_hd.thumbnail.jpg"; + + private OnMediaChange mListener; + private List<MediaItem> mMediaItems = new ArrayList<>(); + + public MediaSourceWPVideos() { + } + + @Override + public void gather(Context context) { + Blog blog = WordPress.getCurrentBlog(); + + if (blog != null) { + Cursor videoCursor = WordPressMediaUtils.getWordPressMediaVideos(String.valueOf(blog.getLocalTableBlogId())); + + if (videoCursor != null) { + addWordPressVideosFromCursor(videoCursor); + videoCursor.close(); + } else if (mListener != null){ + mListener.onMediaLoaded(false); + } + } else if (mListener != null){ + mListener.onMediaLoaded(false); + } + } + + @Override + public void cleanup() { + mMediaItems.clear(); + } + + @Override + public void setListener(OnMediaChange listener) { + mListener = listener; + } + + @Override + public int getCount() { + return mMediaItems.size(); + } + + @Override + public MediaItem getMedia(int position) { + return mMediaItems.get(position); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, ImageLoader.ImageCache cache) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.media_item_wp_video, parent, false); + } + + if (convertView != null) { + MediaItem mediaItem = mMediaItems.get(position); + Uri imageSource = mediaItem.getPreviewSource(); + ImageView imageView = (ImageView) convertView.findViewById(R.id.wp_video_view_background); + if (imageView != null) { + if (imageSource != null) { + Bitmap imageBitmap = null; + if (cache != null) { + imageBitmap = cache.getBitmap(imageSource.toString()); + } + + imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + if (imageBitmap == null) { + imageView.setImageDrawable(placeholderDrawable(convertView.getContext())); + WordPressMediaUtils.BackgroundDownloadWebImage bgDownload = + new WordPressMediaUtils.BackgroundDownloadWebImage(imageView); + imageView.setTag(bgDownload); + bgDownload.execute(mediaItem.getPreviewSource()); + } else { + org.wordpress.mediapicker.MediaUtils.fadeInImage(imageView, imageBitmap); + } + } else { + imageView.setTag(null); + imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + imageView.setImageResource(R.drawable.video_thumbnail); + } + } + } + + return convertView; + } + + @Override + public boolean onMediaItemSelected(MediaItem mediaItem, boolean selected) { + return !selected; + } + + private Drawable placeholderDrawable(Context context) { + if (context != null && context.getResources() != null) { + return context.getResources().getDrawable(R.drawable.media_item_placeholder); + } + + return null; + } + + /** + * Helper method; removes unnecessary characters from videoPressShortcode cursor value + * + * @param cursorEntry + * the cursor value for the videoPressShortcode key + * @return + * the VideoPress code + */ + private String extractVideoPressCode(String cursorEntry) { + cursorEntry = cursorEntry.replace("[wpvideo ", ""); + cursorEntry = cursorEntry.substring(0, cursorEntry.length() - 1); + + return cursorEntry; + } + + private void addWordPressVideosFromCursor(Cursor cursor) { + if (cursor.moveToFirst()) { + do { + int attachmentIdColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID); + int fileUrlColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL); + int fileNameColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME); + int videoPressColumnIndex = cursor.getColumnIndex(WordPressDB.COLUMN_NAME_VIDEO_PRESS_SHORTCODE); + + String id = ""; + if (attachmentIdColumnIndex != -1) { + id = String.valueOf(cursor.getInt(attachmentIdColumnIndex)); + } + MediaItem newContent = new MediaItem(); + newContent.setTag(id); + newContent.setTitle(""); + + if (fileUrlColumnIndex != -1) { + String fileUrl = cursor.getString(fileUrlColumnIndex); + + if (fileUrl != null && MediaUtils.isVideo(fileUrl)) { + newContent.setSource(Uri.parse(fileUrl)); + } else { + continue; + } + } + + if (videoPressColumnIndex != -1 && fileNameColumnIndex != -1) { + String videoPressCode = cursor.getString(videoPressColumnIndex); + String fileName = cursor.getString(fileNameColumnIndex); + + if (videoPressCode != null && !videoPressCode.isEmpty() && fileName != null && !fileName.isEmpty()) { + videoPressCode = extractVideoPressCode(videoPressCode); + newContent.setPreviewSource(VIDEO_PRESS_HOST + videoPressCode + "/" + fileName + VIDEO_PRESS_THUMBNAIL_APPEND); + } + } + + mMediaItems.add(newContent); + } while (cursor.moveToNext()); + } + + if (mListener != null) { + mListener.onMediaLoaded(true); + } + } + + /** + * {@link android.os.Parcelable} interface + */ + + public static final Creator<MediaSourceWPVideos> CREATOR = + new Creator<MediaSourceWPVideos>() { + public MediaSourceWPVideos createFromParcel(Parcel in) { + return new MediaSourceWPVideos(); + } + + public MediaSourceWPVideos[] newArray(int size) { + return new MediaSourceWPVideos[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/WordPressMediaUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/media/WordPressMediaUtils.java new file mode 100644 index 000000000..16c2c5ccc --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/WordPressMediaUtils.java @@ -0,0 +1,379 @@ +package org.wordpress.android.ui.media; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Environment; +import android.provider.MediaStore; +import android.support.v4.content.FileProvider; +import android.widget.ImageView; + +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.NetworkImageView; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.WordPressDB; +import org.wordpress.android.ui.RequestCodes; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; +import org.wordpress.android.util.DeviceUtils; +import org.wordpress.android.util.MediaUtils; +import org.wordpress.android.util.PhotonUtils; +import org.wordpress.android.util.helpers.Version; +import org.wordpress.passcodelock.AppLockManager; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.net.URL; + +import static org.wordpress.mediapicker.MediaUtils.fadeInImage; + +public class WordPressMediaUtils { + public interface LaunchCameraCallback { + void onMediaCapturePathReady(String mediaCapturePath); + } + + private static void showSDCardRequiredDialog(Context context) { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); + dialogBuilder.setTitle(context.getResources().getText(R.string.sdcard_title)); + dialogBuilder.setMessage(context.getResources().getText(R.string.sdcard_message)); + dialogBuilder.setPositiveButton(context.getString(R.string.ok), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + dialog.dismiss(); + } + }); + dialogBuilder.setCancelable(true); + dialogBuilder.create().show(); + } + + public static void launchVideoLibrary(Activity activity) { + AppLockManager.getInstance().setExtendedTimeout(); + activity.startActivityForResult(prepareVideoLibraryIntent(activity), + RequestCodes.VIDEO_LIBRARY); + } + + public static void launchVideoLibrary(Fragment fragment) { + if (!fragment.isAdded()) { + return; + } + AppLockManager.getInstance().setExtendedTimeout(); + fragment.startActivityForResult(prepareVideoLibraryIntent(fragment.getActivity()), + RequestCodes.VIDEO_LIBRARY); + } + + + public static Intent prepareVideoLibraryIntent(Context context) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("video/*"); + return Intent.createChooser(intent, context.getString(R.string.pick_video)); + } + + public static void launchVideoCamera(Activity activity) { + AppLockManager.getInstance().setExtendedTimeout(); + activity.startActivityForResult(prepareVideoCameraIntent(), RequestCodes.TAKE_VIDEO); + } + + public static void launchVideoCamera(Fragment fragment) { + if (!fragment.isAdded()) { + return; + } + AppLockManager.getInstance().setExtendedTimeout(); + fragment.startActivityForResult(prepareVideoCameraIntent(), RequestCodes.TAKE_VIDEO); + } + + private static Intent prepareVideoCameraIntent() { + return new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + } + + public static void launchPictureLibrary(Activity activity) { + AppLockManager.getInstance().setExtendedTimeout(); + activity.startActivityForResult(preparePictureLibraryIntent(activity.getString(R.string.pick_photo)), + RequestCodes.PICTURE_LIBRARY); + } + + public static void launchPictureLibrary(Fragment fragment) { + if (!fragment.isAdded()) { + return; + } + AppLockManager.getInstance().setExtendedTimeout(); + fragment.startActivityForResult(preparePictureLibraryIntent(fragment.getActivity() + .getString(R.string.pick_photo)), RequestCodes.PICTURE_LIBRARY); + } + + private static Intent preparePictureLibraryIntent(String title) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + return Intent.createChooser(intent, title); + } + + private static Intent prepareGalleryIntent(String title) { + Intent intent = new Intent(Intent.ACTION_PICK); + intent.setType("image/*"); + return Intent.createChooser(intent, title); + } + + public static void launchCamera(Activity activity, String applicationId, LaunchCameraCallback callback) { + Intent intent = preparelaunchCamera(activity, applicationId, callback); + if (intent != null) { + AppLockManager.getInstance().setExtendedTimeout(); + activity.startActivityForResult(intent, RequestCodes.TAKE_PHOTO); + } + } + + public static void launchCamera(Fragment fragment, String applicationId, LaunchCameraCallback callback) { + if (!fragment.isAdded()) { + return; + } + Intent intent = preparelaunchCamera(fragment.getActivity(), applicationId, callback); + if (intent != null) { + AppLockManager.getInstance().setExtendedTimeout(); + fragment.startActivityForResult(intent, RequestCodes.TAKE_PHOTO); + } + } + + private static Intent preparelaunchCamera(Context context, String applicationId, LaunchCameraCallback callback) { + String state = android.os.Environment.getExternalStorageState(); + if (!state.equals(android.os.Environment.MEDIA_MOUNTED)) { + showSDCardRequiredDialog(context); + return null; + } else { + return getLaunchCameraIntent(context, applicationId, callback); + } + } + + private static Intent getLaunchCameraIntent(Context context, String applicationId, LaunchCameraCallback callback) { + File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + + String mediaCapturePath = path + File.separator + "Camera" + File.separator + "wp-" + System + .currentTimeMillis() + ".jpg"; + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(context, + applicationId + ".provider", new File(mediaCapturePath))); + + if (callback != null) { + callback.onMediaCapturePathReady(mediaCapturePath); + } + + // make sure the directory we plan to store the recording in exists + File directory = new File(mediaCapturePath).getParentFile(); + if (!directory.exists() && !directory.mkdirs()) { + try { + throw new IOException("Path to file could not be created."); + } catch (IOException e) { + AppLog.e(T.POSTS, e); + } + } + return intent; + } + + public static void launchPictureLibraryOrCapture(Fragment fragment, String applicationId, LaunchCameraCallback + callback) { + if (!fragment.isAdded()) { + return; + } + AppLockManager.getInstance().setExtendedTimeout(); + fragment.startActivityForResult(makePickOrCaptureIntent(fragment.getActivity(), applicationId, callback), + RequestCodes.PICTURE_LIBRARY_OR_CAPTURE); + } + + private static Intent makePickOrCaptureIntent(Context context, String applicationId, LaunchCameraCallback callback) { + Intent pickPhotoIntent = prepareGalleryIntent(context.getString(R.string.capture_or_pick_photo)); + + if (DeviceUtils.getInstance().hasCamera(context)) { + Intent cameraIntent = getLaunchCameraIntent(context, applicationId, callback); + pickPhotoIntent.putExtra( + Intent.EXTRA_INITIAL_INTENTS, + new Intent[]{ cameraIntent }); + } + + return pickPhotoIntent; + } + + public static int getPlaceholder(String url) { + if (MediaUtils.isValidImage(url)) { + return R.drawable.media_image_placeholder; + } else if (MediaUtils.isDocument(url)) { + return R.drawable.media_document; + } else if (MediaUtils.isPowerpoint(url)) { + return R.drawable.media_powerpoint; + } else if (MediaUtils.isSpreadsheet(url)) { + return R.drawable.media_spreadsheet; + } else if (MediaUtils.isVideo(url)) { + return org.wordpress.android.editor.R.drawable.media_movieclip; + } else if (MediaUtils.isAudio(url)) { + return R.drawable.media_audio; + } else { + return 0; + } + } + + /** + * This is a workaround for WP3.4.2 that deletes the media from the server when editing media properties + * within the app. See: https://github.com/wordpress-mobile/WordPress-Android/issues/204 + */ + public static boolean isWordPressVersionWithMediaEditingCapabilities() { + if (WordPress.currentBlog == null) { + return false; + } + + if (WordPress.currentBlog.getWpVersion() == null) { + return true; + } + + if (WordPress.currentBlog.isDotcomFlag()) { + return true; + } + + Version minVersion; + Version currentVersion; + try { + minVersion = new Version("3.5.2"); + currentVersion = new Version(WordPress.currentBlog.getWpVersion()); + + if (currentVersion.compareTo(minVersion) == -1) { + return false; + } + } catch (IllegalArgumentException e) { + AppLog.e(T.POSTS, e); + } + + return true; + } + + public static boolean canDeleteMedia(String blogId, String mediaID) { + Cursor cursor = WordPress.wpDB.getMediaFile(blogId, mediaID); + if (!cursor.moveToFirst()) { + cursor.close(); + return false; + } + String state = cursor.getString(cursor.getColumnIndex("uploadState")); + cursor.close(); + return state == null || !state.equals("uploading"); + } + + public static class BackgroundDownloadWebImage extends AsyncTask<Uri, String, Bitmap> { + WeakReference<ImageView> mReference; + + public BackgroundDownloadWebImage(ImageView resultStore) { + mReference = new WeakReference<>(resultStore); + } + + @Override + protected Bitmap doInBackground(Uri... params) { + try { + String uri = params[0].toString(); + Bitmap bitmap = WordPress.getBitmapCache().getBitmap(uri); + + if (bitmap == null) { + URL url = new URL(uri); + bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream()); + WordPress.getBitmapCache().put(uri, bitmap); + } + + return bitmap; + } + catch(IOException notFoundException) { + return null; + } + } + + @Override + protected void onPostExecute(Bitmap result) { + ImageView imageView = mReference.get(); + + if (imageView != null) { + if (imageView.getTag() == this) { + imageView.setImageBitmap(result); + fadeInImage(imageView, result); + } + } + } + } + + public static Cursor getWordPressMediaImages(String blogId) { + return WordPress.wpDB.getMediaImagesForBlog(blogId); + } + + public static Cursor getWordPressMediaVideos(String blogId) { + return WordPress.wpDB.getMediaFilesForBlog(blogId); + } + + /** + * Given a media file cursor, returns the thumbnail network URL. Will use photon if available, using the specified + * width. + * @param cursor the media file cursor + * @param width width to use for photon request (if applicable) + */ + public static String getNetworkThumbnailUrl(Cursor cursor, int width) { + String thumbnailURL = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_THUMBNAIL_URL)); + + // Allow non-private wp.com and Jetpack blogs to use photon to get a higher res thumbnail + if ((WordPress.getCurrentBlog() != null && WordPress.getCurrentBlog().isPhotonCapable())) { + String imageURL = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_URL)); + if (imageURL != null) { + thumbnailURL = PhotonUtils.getPhotonImageUrl(imageURL, width, 0); + } + } + + return thumbnailURL; + } + + /** + * Loads the given network image URL into the {@link NetworkImageView}, using the default {@link ImageLoader}. + */ + public static void loadNetworkImage(String imageUrl, NetworkImageView imageView) { + loadNetworkImage(imageUrl, imageView, WordPress.imageLoader); + } + + /** + * Loads the given network image URL into the {@link NetworkImageView}. + */ + public static void loadNetworkImage(String imageUrl, NetworkImageView imageView, ImageLoader imageLoader) { + if (imageUrl != null) { + Uri uri = Uri.parse(imageUrl); + String filepath = uri.getLastPathSegment(); + + int placeholderResId = WordPressMediaUtils.getPlaceholder(filepath); + imageView.setErrorImageResId(placeholderResId); + + // no default image while downloading + imageView.setDefaultImageResId(0); + + if (MediaUtils.isValidImage(filepath)) { + imageView.setTag(imageUrl); + imageView.setImageUrl(imageUrl, imageLoader); + } else { + imageView.setImageResource(placeholderResId); + } + } else { + imageView.setImageResource(0); + } + } + + /** + * Returns a poster (thumbnail) URL given a VideoPress video URL + * @param videoUrl the remote URL to the VideoPress video + */ + public static String getVideoPressVideoPosterFromURL(String videoUrl) { + String posterUrl = ""; + + if (videoUrl != null) { + int filetypeLocation = videoUrl.lastIndexOf("."); + if (filetypeLocation > 0) { + posterUrl = videoUrl.substring(0, filetypeLocation) + "_std.original.jpg"; + } + } + + return posterUrl; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaDeleteService.java b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaDeleteService.java new file mode 100644 index 000000000..af0497946 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaDeleteService.java @@ -0,0 +1,121 @@ +package org.wordpress.android.ui.media.services; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.Handler; +import android.os.IBinder; + +import org.wordpress.android.WordPress; +import org.wordpress.android.models.MediaUploadState; +import org.xmlrpc.android.ApiHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * A service for deleting media files from the media browser. + * Only one file is deleted at a time. + */ +public class MediaDeleteService extends Service { + // time to wait before trying to delete the next file + private static final int DELETE_WAIT_TIME = 1000; + + private Context mContext; + private Handler mHandler = new Handler(); + private boolean mDeleteInProgress; + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + + mContext = this.getApplicationContext(); + mDeleteInProgress = false; + } + + @Override + public void onStart(Intent intent, int startId) { + mHandler.post(mFetchQueueTask); + } + + private Runnable mFetchQueueTask = new Runnable() { + @Override + public void run() { + Cursor cursor = getQueueItem(); + try { + if ((cursor == null || cursor.getCount() == 0 || mContext == null) && !mDeleteInProgress) { + MediaDeleteService.this.stopSelf(); + return; + } else { + if (mDeleteInProgress) { + mHandler.postDelayed(this, DELETE_WAIT_TIME); + } else { + deleteMediaFile(cursor); + } + } + } finally { + if (cursor != null) + cursor.close(); + } + + } + }; + + private Cursor getQueueItem() { + if (WordPress.getCurrentBlog() == null) + return null; + + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + return WordPress.wpDB.getMediaDeleteQueueItem(blogId); + } + + private void deleteMediaFile(Cursor cursor) { + if (!cursor.moveToFirst()) + return; + + mDeleteInProgress = true; + + final String blogId = cursor.getString((cursor.getColumnIndex("blogId"))); + final String mediaId = cursor.getString(cursor.getColumnIndex("mediaId")); + + ApiHelper.DeleteMediaTask task = new ApiHelper.DeleteMediaTask(mediaId, + new ApiHelper.GenericCallback() { + @Override + public void onSuccess() { + // only delete them once we get an ok from the server + if (WordPress.getCurrentBlog() != null && mediaId != null) { + WordPress.wpDB.deleteMediaFile(blogId, mediaId); + } + + mDeleteInProgress = false; + mHandler.post(mFetchQueueTask); + } + + @Override + public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) { + // Ideally we would do handle the 401 (unauthorized) and 404 (not found) errors, + // but the XMLRPCExceptions don't seem to give messages when they are thrown. + + // Instead we'll just set them as "deleted" so they don't show up in the delete queue. + // Otherwise the service will continuously try to delete an item they can't delete. + + WordPress.wpDB.updateMediaUploadState(blogId, mediaId, MediaUploadState.DELETED); + + mDeleteInProgress = false; + mHandler.post(mFetchQueueTask); + } + }); + + List<Object> apiArgs = new ArrayList<Object>(); + apiArgs.add(WordPress.getCurrentBlog()); + task.execute(apiArgs) ; + + mHandler.post(mFetchQueueTask); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaEvents.java new file mode 100644 index 000000000..e9ae8d9a7 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaEvents.java @@ -0,0 +1,51 @@ +package org.wordpress.android.ui.media.services; + +public class MediaEvents { + public static class MediaUploadSucceeded { + public final String mLocalBlogId; + public final String mLocalMediaId; + public final String mRemoteMediaId; + public final String mRemoteMediaUrl; + public final String mSecondaryRemoteMediaId; + MediaUploadSucceeded(String localBlogId, String localMediaId, String remoteMediaId, String remoteMediaUrl, + String secondaryRemoteMediaId) { + mLocalBlogId = localBlogId; + mLocalMediaId = localMediaId; + mRemoteMediaId = remoteMediaId; + mRemoteMediaUrl = remoteMediaUrl; + mSecondaryRemoteMediaId = secondaryRemoteMediaId; + } + } + + public static class MediaUploadFailed { + public final String mLocalMediaId; + public final String mErrorMessage; + public final boolean mIsGenericMessage; + MediaUploadFailed(String localMediaId, String errorMessage, boolean isGenericMessage) { + mLocalMediaId = localMediaId; + mErrorMessage = errorMessage; + mIsGenericMessage = isGenericMessage; + } + MediaUploadFailed(String localMediaId, String errorMessage) { + this(localMediaId, errorMessage, false); + } + } + + public static class MediaUploadProgress { + public final String mLocalMediaId; + public final float mProgress; + MediaUploadProgress(String localMediaId, float progress) { + mLocalMediaId = localMediaId; + mProgress = progress; + } + } + + public static class MediaChanged { + public final String mLocalBlogId; + public final String mMediaId; + public MediaChanged(String localBlogId, String mediaId) { + mLocalBlogId = localBlogId; + mMediaId = mediaId; + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadService.java b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadService.java new file mode 100644 index 000000000..11d0ccae5 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/services/MediaUploadService.java @@ -0,0 +1,247 @@ +package org.wordpress.android.ui.media.services; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.Handler; +import android.os.IBinder; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.WordPressDB; +import org.wordpress.android.models.MediaUploadState; +import org.wordpress.android.ui.media.services.MediaEvents.MediaChanged; +import org.wordpress.android.util.AppLog.T; +import org.wordpress.android.util.CrashlyticsUtils; +import org.wordpress.android.util.CrashlyticsUtils.ExceptionType; +import org.wordpress.android.util.helpers.MediaFile; +import org.xmlrpc.android.ApiHelper; +import org.xmlrpc.android.ApiHelper.ErrorType; +import org.xmlrpc.android.ApiHelper.GetMediaItemTask; + +import java.util.ArrayList; +import java.util.List; + +import de.greenrobot.event.EventBus; + +/** + * A service for uploading media files from the media browser. + * Only one file is uploaded at a time. + */ +public class MediaUploadService extends Service { + // time to wait before trying to upload the next file + private static final int UPLOAD_WAIT_TIME = 1000; + + private static MediaUploadService mInstance; + + private Context mContext; + private Handler mHandler = new Handler(); + + private boolean mUploadInProgress; + private ApiHelper.UploadMediaTask mCurrentUploadMediaTask; + private String mCurrentUploadMediaId; + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + + mInstance = this; + + mContext = this.getApplicationContext(); + mUploadInProgress = false; + + cancelOldUploads(); + } + + @Override + public void onStart(Intent intent, int startId) { + mHandler.post(mFetchQueueTask); + } + + public static MediaUploadService getInstance() { + return mInstance; + } + + public void processQueue() { + mHandler.post(mFetchQueueTask); + } + + /** + * Returns whether the service has any media uploads in progress or queued. + */ + public boolean hasUploads() { + if (mUploadInProgress) { + return true; + } else { + Cursor queueCursor = getQueue(); + return (queueCursor == null || queueCursor.getCount() > 0); + } + } + + /** + * Cancel the upload with the given id, whether it's currently uploading or queued. + * @param mediaId the id of the media item + * @param delete whether to delete the item from the queue or mark it as failed so it can be retried later + */ + public void cancelUpload(String mediaId, boolean delete) { + if (mediaId.equals(mCurrentUploadMediaId)) { + // The media item is currently uploading - abort the upload process + mCurrentUploadMediaTask.cancel(true); + mUploadInProgress = false; + } else { + // Remove the media item from the upload queue + if (WordPress.getCurrentBlog() != null) { + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + if (delete) { + WordPress.wpDB.deleteMediaFile(blogId, mediaId); + } else { + WordPress.wpDB.updateMediaUploadState(blogId, mediaId, MediaUploadState.FAILED); + } + } + } + } + + private Runnable mFetchQueueTask = new Runnable() { + @Override + public void run() { + Cursor cursor = getQueue(); + try { + if ((cursor == null || cursor.getCount() == 0 || mContext == null) && !mUploadInProgress) { + MediaUploadService.this.stopSelf(); + return; + } else { + if (mUploadInProgress) { + mHandler.postDelayed(this, UPLOAD_WAIT_TIME); + } else { + uploadMediaFile(cursor); + } + } + } finally { + if (cursor != null) + cursor.close(); + } + + } + }; + + private void cancelOldUploads() { + // There should be no media files with an upload state of 'uploading' at the start of this service. + // Since we won't be able to receive notifications for these, set them to 'failed'. + + if (WordPress.getCurrentBlog() != null) { + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + WordPress.wpDB.setMediaUploadingToFailed(blogId); + } + } + + private Cursor getQueue() { + if (WordPress.getCurrentBlog() == null) + return null; + + String blogId = String.valueOf(WordPress.getCurrentBlog().getLocalTableBlogId()); + return WordPress.wpDB.getMediaUploadQueue(blogId); + } + + private void uploadMediaFile(Cursor cursor) { + if (!cursor.moveToFirst()) + return; + + mUploadInProgress = true; + + final String blogIdStr = cursor.getString((cursor.getColumnIndex(WordPressDB.COLUMN_NAME_BLOG_ID))); + final String mediaId = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MEDIA_ID)); + String fileName = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_NAME)); + String filePath = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_FILE_PATH)); + String mimeType = cursor.getString(cursor.getColumnIndex(WordPressDB.COLUMN_NAME_MIME_TYPE)); + + MediaFile mediaFile = new MediaFile(); + mediaFile.setBlogId(blogIdStr); + mediaFile.setFileName(fileName); + mediaFile.setFilePath(filePath); + mediaFile.setMimeType(mimeType); + + mCurrentUploadMediaId = mediaId; + + mCurrentUploadMediaTask = new ApiHelper.UploadMediaTask(mContext, mediaFile, + new ApiHelper.UploadMediaTask.Callback() { + @Override + public void onSuccess(String remoteId, String remoteUrl, String secondaryId) { + // once the file has been uploaded, update the local database entry (swap the id with the remote id) + // and download the new one + WordPress.wpDB.updateMediaLocalToRemoteId(blogIdStr, mediaId, remoteId); + EventBus.getDefault().post(new MediaEvents.MediaUploadSucceeded(blogIdStr, mediaId, + remoteId, remoteUrl, secondaryId)); + fetchMediaFile(remoteId); + } + + @Override + public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) { + WordPress.wpDB.updateMediaUploadState(blogIdStr, mediaId, MediaUploadState.FAILED); + mUploadInProgress = false; + mCurrentUploadMediaId = ""; + + MediaEvents.MediaUploadFailed event; + if (errorMessage == null) { + event = new MediaEvents.MediaUploadFailed(mediaId, getString(R.string.upload_failed), true); + } else { + event = new MediaEvents.MediaUploadFailed(mediaId, errorMessage); + } + + EventBus.getDefault().post(event); + mHandler.post(mFetchQueueTask); + + // Only log the error if it's not caused by the network (internal inconsistency) + if (errorType != ErrorType.NETWORK_XMLRPC) { + CrashlyticsUtils.logException(throwable, ExceptionType.SPECIFIC, T.MEDIA, errorMessage); + } + } + + @Override + public void onProgressUpdate(float progress) { + EventBus.getDefault().post(new MediaEvents.MediaUploadProgress(mediaId, progress)); + } + }); + + WordPress.wpDB.updateMediaUploadState(blogIdStr, mediaId, MediaUploadState.UPLOADING); + List<Object> apiArgs = new ArrayList<Object>(); + apiArgs.add(WordPress.getCurrentBlog()); + mCurrentUploadMediaTask.execute(apiArgs); + mHandler.post(mFetchQueueTask); + } + + private void fetchMediaFile(final String id) { + List<Object> apiArgs = new ArrayList<Object>(); + apiArgs.add(WordPress.getCurrentBlog()); + GetMediaItemTask task = new GetMediaItemTask(Integer.valueOf(id), + new ApiHelper.GetMediaItemTask.Callback() { + @Override + public void onSuccess(MediaFile mediaFile) { + String blogId = mediaFile.getBlogId(); + String mediaId = mediaFile.getMediaId(); + WordPress.wpDB.updateMediaUploadState(blogId, mediaId, MediaUploadState.UPLOADED); + mUploadInProgress = false; + mCurrentUploadMediaId = ""; + mHandler.post(mFetchQueueTask); + EventBus.getDefault().post(new MediaChanged(blogId, mediaId)); + } + + @Override + public void onFailure(ApiHelper.ErrorType errorType, String errorMessage, Throwable throwable) { + mUploadInProgress = false; + mCurrentUploadMediaId = ""; + mHandler.post(mFetchQueueTask); + // Only log the error if it's not caused by the network (internal inconsistency) + if (errorType != ErrorType.NETWORK_XMLRPC) { + CrashlyticsUtils.logException(throwable, ExceptionType.SPECIFIC, T.MEDIA, errorMessage); + } + } + }); + task.execute(apiArgs); + } +} |