├── sample ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ ├── styles.xml │ │ │ │ ├── integers.xml │ │ │ │ └── dimens.xml │ │ │ ├── drawable-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── values-w600dp │ │ │ │ └── integers.xml │ │ │ ├── menu │ │ │ │ └── menu_media_picker_action_mode.xml │ │ │ └── layout │ │ │ │ ├── activity_media_picker_sample.xml │ │ │ │ ├── media_fragment_four_grid.xml │ │ │ │ └── media_fragment_three_grid.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── org │ │ │ └── wordpress │ │ │ └── mediapickersample │ │ │ ├── source │ │ │ ├── MediaSourceEmpty.java │ │ │ └── MediaSourceError.java │ │ │ ├── SampleActivity.java │ │ │ ├── SlidingTabStrip.java │ │ │ └── SlidingTabLayout.java │ └── androidTest │ │ └── java │ │ └── org │ │ └── wordpress │ │ └── mediapickersample │ │ └── ApplicationTest.java ├── build.gradle └── proguard-rules.pro ├── mediapicker ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── drawable-xxhdpi │ │ │ │ ├── video.png │ │ │ │ ├── camera.png │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_media_play.png │ │ │ │ ├── ic_now_wallpaper_white.png │ │ │ │ ├── tab_icon_create_gallery.png │ │ │ │ └── action_mode_confirm_checkmark.png │ │ │ ├── drawable-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_media_play.png │ │ │ │ ├── tab_icon_create_gallery.png │ │ │ │ └── action_mode_confirm_checkmark.png │ │ │ ├── drawable-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_media_play.png │ │ │ │ ├── tab_icon_create_gallery.png │ │ │ │ ├── action_mode_confirm_checkmark.png │ │ │ │ └── tab_unselected_pressed_wordpress.9.png │ │ │ ├── values │ │ │ │ ├── integers.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── dimens.xml │ │ │ ├── drawable │ │ │ │ ├── media_picker_background.xml │ │ │ │ ├── media_item_placeholder.xml │ │ │ │ └── media_item_frame_selector.xml │ │ │ ├── menu │ │ │ │ └── media_picker_action_mode.xml │ │ │ └── layout │ │ │ │ ├── media_picker_fragment.xml │ │ │ │ ├── media_item_image.xml │ │ │ │ ├── media_item_capture.xml │ │ │ │ └── media_item_video.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── org │ │ │ └── wordpress │ │ │ └── mediapicker │ │ │ ├── source │ │ │ ├── MediaSourceCaptureVideo.java │ │ │ ├── MediaSource.java │ │ │ ├── MediaSourceDeviceVideos.java │ │ │ ├── CameraPreview.java │ │ │ ├── MediaSourceDeviceImages.java │ │ │ └── MediaSourceCaptureImage.java │ │ │ ├── CheckableFrameLayout.java │ │ │ ├── MediaSourceAdapter.java │ │ │ ├── MediaItem.java │ │ │ ├── MediaUtils.java │ │ │ └── MediaPickerFragment.java │ └── androidTest │ │ └── java │ │ └── org │ │ └── wordpress │ │ └── mediapicker │ │ ├── source │ │ └── MediaSourceDeviceImagesTest.java │ │ ├── MediaUtilsTest.java │ │ ├── MediaPickerFragmentTest.java │ │ ├── MediaSourceAdapterTest.java │ │ └── MediaItemTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /mediapicker/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':mediapicker' 2 | include ':sample' 3 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Media Picker Sample 4 | 5 | 6 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xxhdpi/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xxhdpi/video.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xxhdpi/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xxhdpi/camera.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/sample/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-hdpi/ic_media_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-hdpi/ic_media_play.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xhdpi/ic_media_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xhdpi/ic_media_play.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xxhdpi/ic_media_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xxhdpi/ic_media_play.png -------------------------------------------------------------------------------- /sample/src/main/res/values-w600dp/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 5 6 | 7 | 8 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-hdpi/tab_icon_create_gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-hdpi/tab_icon_create_gallery.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xhdpi/tab_icon_create_gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xhdpi/tab_icon_create_gallery.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xxhdpi/ic_now_wallpaper_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xxhdpi/ic_now_wallpaper_white.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xxhdpi/tab_icon_create_gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xxhdpi/tab_icon_create_gallery.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-hdpi/action_mode_confirm_checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-hdpi/action_mode_confirm_checkmark.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xhdpi/action_mode_confirm_checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xhdpi/action_mode_confirm_checkmark.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xxhdpi/action_mode_confirm_checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xxhdpi/action_mode_confirm_checkmark.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable-xhdpi/tab_unselected_pressed_wordpress.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wordpress-mobile/MediaPicker-Android/HEAD/mediapicker/src/main/res/drawable-xhdpi/tab_unselected_pressed_wordpress.9.png -------------------------------------------------------------------------------- /mediapicker/src/main/res/values/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 2 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/values/integers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 3 7 | 8 | 9 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable/media_picker_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable/media_item_placeholder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #999999 7 | #a9f9f9 8 | 9 | 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /mediapicker/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 4dp 7 | 8dp 8 | 6dp 9 | 10 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/drawable/media_item_frame_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/org/wordpress/mediapickersample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapickersample; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /mediapicker/src/main/res/menu/media_picker_action_mode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | gradle.properties 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Intellij project files 24 | *.iml 25 | *.ipr 26 | *.iws 27 | .idea/ 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.0" 6 | 7 | defaultConfig { 8 | applicationId "org.wordpress.mediapickersample" 9 | minSdkVersion 14 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "0.5" 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile 'com.android.support:appcompat-v7:22.2.1' 25 | compile project(':mediapicker') 26 | } 27 | -------------------------------------------------------------------------------- /sample/src/main/res/menu/menu_media_picker_action_mode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/tonyr/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /mediapicker/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/tonyr/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Media Picker 4 | 5 | 6 | Create gallery 7 | Confirm 8 | 9 | 10 | media item image 11 | media item overlay image 12 | 13 | Fetching media 14 | No media sources set. 15 | No media. Why not take capture some? 16 | Error gathering media from source 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_media_picker_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 12 | 13 | 15 | 16 | 17 | 18 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/layout/media_picker_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 16 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/media_fragment_four_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 16 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/media_fragment_three_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 16 | 17 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/source/MediaSourceCaptureVideo.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker.source; 2 | 3 | import android.os.Parcel; 4 | 5 | import org.wordpress.mediapicker.R; 6 | 7 | public class MediaSourceCaptureVideo extends MediaSourceCaptureImage { 8 | @Override 9 | protected int getOverlayResource() { 10 | return R.drawable.video; 11 | } 12 | 13 | public static final Creator CREATOR = 14 | new Creator() { 15 | public MediaSourceCaptureVideo createFromParcel(Parcel in) { 16 | return new MediaSourceCaptureVideo(); 17 | } 18 | 19 | public MediaSourceCaptureVideo[] newArray(int size) { 20 | return new MediaSourceCaptureVideo[size]; 21 | } 22 | }; 23 | 24 | @Override 25 | public int describeContents() { 26 | return 0; 27 | } 28 | 29 | @Override 30 | public void writeToParcel(Parcel dest, int flags) { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/CheckableFrameLayout.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.Checkable; 6 | import android.widget.FrameLayout; 7 | 8 | /** 9 | * Frame layout that manages a checked state. 10 | */ 11 | 12 | public class CheckableFrameLayout extends FrameLayout implements Checkable { 13 | private boolean mIsChecked; 14 | 15 | public CheckableFrameLayout(Context context, AttributeSet attrs) { 16 | super(context, attrs); 17 | 18 | mIsChecked = false; 19 | } 20 | 21 | @Override 22 | public void setChecked(boolean checked) { 23 | mIsChecked = checked; 24 | refreshDrawableState(); 25 | } 26 | 27 | @Override 28 | public boolean isChecked() { 29 | return mIsChecked; 30 | } 31 | 32 | @Override 33 | public void toggle() { 34 | mIsChecked = !mIsChecked; 35 | } 36 | 37 | @Override 38 | protected int[] onCreateDrawableState(int extraSpace) { 39 | final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 40 | 41 | if (isChecked()) { 42 | mergeDrawableStates(drawableState, new int[] { android.R.attr.state_checked }); 43 | } 44 | 45 | return drawableState; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 24sp 6 | 7 | 8 | 2dp 9 | 2dp 10 | 2dp 11 | 2dp 12 | 2dp 13 | 2dp 14 | 15 | 16 | 128dp 17 | 18 | 19 | 20 | 21 | 2dp 22 | 2dp 23 | 2dp 24 | 2dp 25 | 6dp 26 | 6dp 27 | 6dp 28 | 6dp 29 | 30 | 31 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/layout/media_item_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | Note: This library has been deprecated, support and maintainence will no longer be provided. 4 | 5 | # MediaPicker 6 | MediaPicker is an Android Fragment that allows capture and selection of media assets from a variety of sources. 7 | 8 | # Features 9 | * Select individual or multiple media items (image or video) 10 | * Capture new media content during selection 11 | * Configuration of which media content is presented; both media source and content type 12 | * MediaSource interface to provide media from anywhere 13 | 14 | ## Usage 15 | The MediaPicker library comes with two MediaSources for accessing media on device view the MediaStore. 16 | 17 | The sample project is an example of how to configure the MediaPickerFragment to display different sets of media content (one for images, one for videos). 18 | 19 | ## Testing 20 | Much of the functionality of the library is tested. You can run tests and generate a code coverage report by running the jacocoTestReport task. 21 | 22 | [Robolectric][2] is used to unit test Android elements in isolation, please use this framework when writing new tests to keep them lightweight. 23 | 24 | ## Installation 25 | The MediaPicker library is hosted on Maven Central. Example build.gradle dependency: 26 | >dependencies {
    compile 'org.wordpress:mediapicker:1.+'
} 27 | 28 | ## Author(s) 29 | WordPress, mobile@automattic.com 30 | 31 | ## License(s) 32 | MediaPicker is available under the GNU GPL v2 or MIT licenses. 33 | 34 | ## Apps that use this library 35 | 36 | * [WordPress for Android][1] 37 | 38 | [1]: https://github.com/wordpress-mobile/WordPress-Android 39 | [2]: http://robolectric.org/ 40 | -------------------------------------------------------------------------------- /sample/src/main/java/org/wordpress/mediapickersample/source/MediaSourceEmpty.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapickersample.source; 2 | 3 | import android.content.Context; 4 | import android.os.Parcel; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import com.android.volley.toolbox.ImageLoader; 10 | 11 | import org.wordpress.mediapicker.MediaItem; 12 | import org.wordpress.mediapicker.source.MediaSource; 13 | 14 | /** 15 | * Demo class that contains no media data. 16 | */ 17 | 18 | public class MediaSourceEmpty implements MediaSource { 19 | private OnMediaChange mListener; 20 | 21 | @Override 22 | public void gather(Context context) { 23 | if (mListener != null) { 24 | mListener.onMediaLoaded(true); 25 | } 26 | } 27 | 28 | @Override 29 | public void cleanup() { 30 | } 31 | 32 | @Override 33 | public void setListener(OnMediaChange listener) { 34 | mListener = listener; 35 | } 36 | 37 | @Override 38 | public int getCount() { 39 | return 0; 40 | } 41 | 42 | @Override 43 | public MediaItem getMedia(int position) { 44 | return null; 45 | } 46 | 47 | @Override 48 | public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, ImageLoader.ImageCache cache) { 49 | return null; 50 | } 51 | 52 | @Override 53 | public boolean onMediaItemSelected(MediaItem mediaItem, boolean selected) { 54 | return false; 55 | } 56 | 57 | @Override 58 | public int describeContents() { 59 | return 0; 60 | } 61 | 62 | @Override 63 | public void writeToParcel(Parcel dest, int flags) { 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /sample/src/main/java/org/wordpress/mediapickersample/source/MediaSourceError.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapickersample.source; 2 | 3 | import android.content.Context; 4 | import android.os.Parcel; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import com.android.volley.toolbox.ImageLoader; 10 | 11 | import org.wordpress.mediapicker.MediaItem; 12 | import org.wordpress.mediapicker.source.MediaSource; 13 | 14 | /** 15 | * Demo class that mocks a failed load. 16 | */ 17 | 18 | public class MediaSourceError implements MediaSource { 19 | private OnMediaChange mListener; 20 | 21 | @Override 22 | public void gather(Context context) { 23 | if (mListener != null) { 24 | mListener.onMediaLoaded(false); 25 | } 26 | } 27 | 28 | @Override 29 | public void cleanup() { 30 | } 31 | 32 | @Override 33 | public void setListener(OnMediaChange listener) { 34 | mListener = listener; 35 | } 36 | 37 | @Override 38 | public int getCount() { 39 | return 0; 40 | } 41 | 42 | @Override 43 | public MediaItem getMedia(int position) { 44 | return null; 45 | } 46 | 47 | @Override 48 | public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, ImageLoader.ImageCache cache) { 49 | return null; 50 | } 51 | 52 | @Override 53 | public boolean onMediaItemSelected(MediaItem mediaItem, boolean selected) { 54 | return false; 55 | } 56 | 57 | @Override 58 | public int describeContents() { 59 | return 0; 60 | } 61 | 62 | @Override 63 | public void writeToParcel(Parcel dest, int flags) { 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/layout/media_item_capture.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 25 | 26 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /mediapicker/src/main/res/layout/media_item_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 26 | 27 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /mediapicker/src/androidTest/java/org/wordpress/mediapicker/source/MediaSourceDeviceImagesTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker.source; 2 | 3 | import android.os.Parcel; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | import org.mockito.invocation.InvocationOnMock; 10 | import org.mockito.stubbing.Answer; 11 | import org.robolectric.RobolectricTestRunner; 12 | import org.robolectric.annotation.Config; 13 | import org.wordpress.mediapicker.MediaItem; 14 | 15 | import java.util.List; 16 | 17 | import static org.mockito.Matchers.anyListOf; 18 | import static org.mockito.Matchers.eq; 19 | import static org.mockito.Mockito.doAnswer; 20 | import static org.mockito.Mockito.mock; 21 | 22 | @Config(emulateSdk = 18) 23 | @RunWith(RobolectricTestRunner.class) 24 | public class MediaSourceDeviceImagesTest { 25 | /** 26 | * Verifies that the CREATOR correctly constructs a {@link org.wordpress.mediapicker.source.MediaSourceDeviceImages} 27 | * from valid {@link android.os.Parcel} data. 28 | */ 29 | @Test 30 | public void testCreator() { 31 | final String testTitle = "test-title"; 32 | final String testTag = "test-tag"; 33 | final String testSource = "test-source"; 34 | final String testPreview = "test-preview"; 35 | final int testRotation = 180; 36 | final MediaItem testItem = new MediaItem(); 37 | 38 | testItem.setTag(testTag); 39 | testItem.setTitle(testTitle); 40 | testItem.setSource(testSource); 41 | testItem.setPreviewSource(testPreview); 42 | testItem.setRotation(testRotation); 43 | 44 | final Parcel mockParcel = mock(Parcel.class); 45 | 46 | doAnswer(new Answer() { 47 | @Override 48 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 49 | Object parameter = invocationOnMock.getArguments()[0]; 50 | Assert.assertTrue(parameter instanceof List); 51 | List parcelData = (List) parameter; 52 | 53 | parcelData.add(testItem); 54 | 55 | return null; 56 | } 57 | }).when(mockParcel).readTypedList(anyListOf(MediaItem.class), eq(MediaItem.CREATOR)); 58 | 59 | final MediaSourceDeviceImages testMediaSource = MediaSourceDeviceImages.CREATOR.createFromParcel(mockParcel); 60 | final MediaItem actualItem = testMediaSource.getMedia(0); 61 | 62 | Assert.assertEquals(testTag, actualItem.getTag()); 63 | Assert.assertEquals(testTitle, actualItem.getTitle()); 64 | Assert.assertEquals(testSource, actualItem.getSource().toString()); 65 | Assert.assertEquals(testPreview, actualItem.getPreviewSource().toString()); 66 | Assert.assertEquals(testRotation, actualItem.getRotation()); 67 | } 68 | 69 | /** 70 | * Verifies that no media is present when no {@link android.content.ContentResolver} is given. 71 | */ 72 | @Test 73 | public void testNoMedia() { 74 | final MediaSourceDeviceImages testSource = new MediaSourceDeviceImages(); 75 | 76 | Assert.assertEquals(0, testSource.getCount()); 77 | Assert.assertEquals(null, testSource.getMedia(0)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/source/MediaSource.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker.source; 2 | 3 | import android.content.Context; 4 | import android.os.Parcelable; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import com.android.volley.toolbox.ImageLoader; 10 | 11 | import org.wordpress.mediapicker.MediaItem; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * MediaSource's are used to gather {@link org.wordpress.mediapicker.MediaItem}'s and create 17 | * {@link android.view.View}'s for displaying them. 18 | */ 19 | 20 | public interface MediaSource extends Parcelable { 21 | /** 22 | * Interface offered for any class to implement a listener for data set changes. 23 | */ 24 | public interface OnMediaChange { 25 | /** 26 | * To be called after all initial {@link org.wordpress.mediapicker.MediaItem}s have been 27 | * gathered following a call to {@link #gather(android.content.Context)}. 28 | * 29 | * @param success 30 | * if false an error message will be displayed in lieu of the media views 31 | */ 32 | public void onMediaLoaded(boolean success); 33 | 34 | /** 35 | * To be called when new MediaItems have been added to the source. 36 | * 37 | * @param source 38 | * the host source 39 | * @param addedItems 40 | * the newly added {@link org.wordpress.mediapicker.MediaItem}'s 41 | */ 42 | public void onMediaAdded(MediaSource source, List addedItems); 43 | 44 | /** 45 | * To be called when existing {@link org.wordpress.mediapicker.MediaItem}'s are removed. 46 | * 47 | * @param source 48 | * the host source 49 | * @param removedItems 50 | * the removed {@link org.wordpress.mediapicker.MediaItem}'s 51 | */ 52 | public void onMediaRemoved(MediaSource source, List removedItems); 53 | 54 | /** 55 | * To be called when existing {@link org.wordpress.mediapicker.MediaItem}'s are modified. 56 | * 57 | * @param source 58 | * the host source 59 | * @param changedItems 60 | * the changed {@link org.wordpress.mediapicker.MediaItem}'s 61 | */ 62 | public void onMediaChanged(MediaSource source, List changedItems); 63 | } 64 | 65 | // Load MediaItem data 66 | public void gather(Context context); 67 | // Destroy MediaItem data 68 | public void cleanup(); 69 | // Can be ignored if no listener is needed 70 | public void setListener(final OnMediaChange listener); 71 | // Get the number of MediaItems accessible through the source 72 | public int getCount(); 73 | // Get the MediaItem at the specified position 74 | public MediaItem getMedia(int position); 75 | // Get the View to display the MediaItem at the specified position TODO: remove cache from here 76 | public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, ImageLoader.ImageCache cache); 77 | // Callback when MediaItem is selected; return true if handled internally, false to propagate 78 | public boolean onMediaItemSelected(final MediaItem mediaItem, boolean selected); 79 | } 80 | -------------------------------------------------------------------------------- /mediapicker/src/androidTest/java/org/wordpress/mediapicker/MediaUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker; 2 | 3 | import android.graphics.Bitmap; 4 | import android.net.Uri; 5 | import android.view.animation.Animation; 6 | import android.widget.ImageView; 7 | 8 | import com.android.volley.toolbox.ImageLoader; 9 | 10 | import org.junit.Assert; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | 15 | import org.mockito.invocation.InvocationOnMock; 16 | import org.mockito.stubbing.Answer; 17 | import org.robolectric.RobolectricTestRunner; 18 | import org.robolectric.annotation.Config; 19 | 20 | import static org.mockito.Matchers.any; 21 | import static org.mockito.Mockito.doAnswer; 22 | import static org.mockito.Mockito.mock; 23 | import static org.mockito.Mockito.when; 24 | 25 | @Config(emulateSdk = 18) 26 | @RunWith(RobolectricTestRunner.class) 27 | public class MediaUtilsTest { 28 | private static long FETCH_TIME = 2500; 29 | 30 | private boolean tImageAnimationStarted; 31 | private boolean tImageCached; 32 | 33 | @Before 34 | public void setUp() { 35 | tImageAnimationStarted = false; 36 | tImageCached = false; 37 | } 38 | 39 | /** 40 | * Verifies that a background image fetch fades the fetched image with the ImageView. 41 | */ 42 | @Test 43 | public void testBackgroundImageFetch() { 44 | final Uri testUri = Uri.parse("file://test.jpg"); 45 | final ImageView mockImage = mock(ImageView.class); 46 | final MediaUtils.BackgroundFetchThumbnail testFetch = 47 | new MediaUtils.BackgroundFetchThumbnail(mockImage, null, MediaUtils.BackgroundFetchThumbnail.TYPE_IMAGE, 0, 0, 0); 48 | 49 | doAnswer(new Answer() { 50 | @Override 51 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 52 | tImageAnimationStarted = true; 53 | 54 | return null; 55 | } 56 | }).when(mockImage).startAnimation(any(Animation.class)); 57 | 58 | when(mockImage.getTag()).thenReturn(testFetch); 59 | testFetch.execute(testUri); 60 | 61 | long startTime = System.currentTimeMillis(); 62 | while (!tImageAnimationStarted && System.currentTimeMillis() - startTime < FETCH_TIME); 63 | 64 | Assert.assertTrue(tImageAnimationStarted); 65 | } 66 | 67 | /** 68 | * Verifies that fetched images are cached. 69 | */ 70 | @Test 71 | public void testImageCached() { 72 | final Uri testUri = Uri.parse("file://test.jpg"); 73 | final ImageView mockImage = mock(ImageView.class); 74 | final ImageLoader.ImageCache testCache = new TestImageCache(); 75 | final MediaUtils.BackgroundFetchThumbnail testFetch = 76 | new MediaUtils.BackgroundFetchThumbnail(mockImage, testCache, MediaUtils.BackgroundFetchThumbnail.TYPE_IMAGE, 0, 0, 0); 77 | 78 | when(mockImage.getTag()).thenReturn(testFetch); 79 | testFetch.execute(testUri); 80 | 81 | long startTime = System.currentTimeMillis(); 82 | while (!tImageAnimationStarted && System.currentTimeMillis() - startTime < FETCH_TIME); 83 | 84 | Assert.assertTrue(tImageCached); 85 | } 86 | 87 | /** 88 | * Verifies that {@link org.wordpress.mediapicker.MediaUtils#fadeInImage(android.widget.ImageView, android.graphics.Bitmap)} 89 | * starts an animation on the ImageView. 90 | */ 91 | @Test 92 | public void testFadeInImage() { 93 | final ImageView mockImage = mock(ImageView.class); 94 | final Bitmap mockBitmap = mock(Bitmap.class); 95 | 96 | doAnswer(new Answer() { 97 | @Override 98 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 99 | tImageAnimationStarted = true; 100 | 101 | return null; 102 | } 103 | }).when(mockImage).startAnimation(any(Animation.class)); 104 | 105 | MediaUtils.fadeInImage(mockImage, mockBitmap); 106 | 107 | Assert.assertTrue(tImageAnimationStarted); 108 | } 109 | 110 | /** 111 | * Dummy class for testing image caching. 112 | */ 113 | private class TestImageCache implements ImageLoader.ImageCache { 114 | @Override 115 | public Bitmap getBitmap(String url) { 116 | return null; 117 | } 118 | 119 | @Override 120 | public void putBitmap(String url, Bitmap bitmap) { 121 | tImageCached = true; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /mediapicker/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'org.robolectric:robolectric-gradle-plugin:0.14.1' 7 | } 8 | } 9 | 10 | apply plugin: 'com.android.library' 11 | apply plugin: 'robolectric' 12 | apply plugin: 'maven' 13 | apply plugin: 'signing' 14 | 15 | version = "1.2.4" 16 | group = "org.wordpress" 17 | archivesBaseName = "mediapicker" 18 | 19 | repositories { 20 | mavenCentral() 21 | } 22 | 23 | dependencies { 24 | compile 'com.android.support:support-v13:22.0.0' 25 | compile 'com.mcxiaoke.volley:library:1.0.10' 26 | 27 | androidTestCompile 'junit:junit:4.10' 28 | androidTestCompile 'org.mockito:mockito-core:1.10.19' 29 | androidTestCompile 'org.robolectric:robolectric:2.4' 30 | 31 | // Workaround for IDE bug 32 | // http://stackoverflow.com/questions/22246183/android-studio-doesnt-recognize-espresso-classes 33 | provided 'junit:junit:4.10' 34 | provided 'org.mockito:mockito-core:1.10.19' 35 | provided 'org.robolectric:robolectric:2.4' 36 | } 37 | 38 | android { 39 | publishNonDefault true 40 | 41 | compileSdkVersion 22 42 | buildToolsVersion "22.0.0" 43 | 44 | defaultConfig { 45 | minSdkVersion 14 46 | targetSdkVersion 22 47 | versionCode 11 48 | versionName "1.2.4" 49 | } 50 | 51 | buildTypes { 52 | release { 53 | minifyEnabled false 54 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 55 | } 56 | } 57 | } 58 | 59 | robolectric { 60 | include '**/*Test.class' 61 | } 62 | 63 | apply plugin: 'jacoco' 64 | 65 | jacoco { 66 | toolVersion = "0.7.1.201405082137" 67 | } 68 | 69 | def coverageSourceDirs = ['src/main/java'] 70 | def coverageExclusions = ['**/R.class', 71 | '**/R$*.class', 72 | '**/*$ViewInjector*.*', 73 | '**/BuildConfig.*', 74 | '**/Manifest*.*', 75 | '**/CameraPreview*.*', 76 | '**/MediaSourceCaptureImage*.*', 77 | '**/MediaSourceCaptureImage*.*'] 78 | 79 | task jacocoTestReport(type: JacocoReport, dependsOn: "testDebug") { 80 | group = "Reporting" 81 | description = "Generate Jacoco coverage reports" 82 | 83 | classDirectories = fileTree( 84 | dir: 'build/intermediates/classes/debug', 85 | excludes: coverageExclusions 86 | ) 87 | 88 | additionalSourceDirs = files(coverageSourceDirs) 89 | sourceDirectories = files(coverageSourceDirs) 90 | executionData = files('build/jacoco/testDebug.exec') 91 | 92 | reports { 93 | xml.enabled = true 94 | html.enabled = true 95 | } 96 | } 97 | 98 | signing { 99 | required { 100 | project.properties.containsKey("signing.keyId") && project.properties.containsKey("signing.secretKeyRingFile") 101 | } 102 | sign configurations.archives 103 | } 104 | 105 | uploadArchives { 106 | repositories { 107 | mavenDeployer { 108 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 109 | 110 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { 111 | authentication(userName: project.properties.ossrhUsername, password: project.properties.ossrhPassword) 112 | } 113 | 114 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { 115 | authentication(userName: project.properties.ossrhUsername, password: project.properties.ossrhPassword) 116 | } 117 | 118 | pom.project { 119 | name 'MediaPicker' 120 | packaging 'aar' 121 | description 'MediaPicker is an Android Fragment that enables users to select multiple media from a variety of sources.' 122 | url 'https://github.com/wordpress-mobile/MediaPicker' 123 | 124 | scm { 125 | url 'https://github.com/wordpress-mobile/MediaPicker' 126 | connection 'scm:git:https://github.com/wordpress-mobile/MediaPicker.git' 127 | developerConnection 'scm:git:https://github.com/wordpress-mobile/MediaPicker.git' 128 | } 129 | 130 | licenses { 131 | license { 132 | name 'The MIT License (MIT)' 133 | url 'http://opensource.org/licenses/MIT' 134 | } 135 | } 136 | 137 | developers { 138 | developer { 139 | id 'tonyr59h' 140 | name 'Anthony Rankin' 141 | email 'tonyr@automattic.com' 142 | } 143 | } 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/source/MediaSourceDeviceVideos.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker.source; 2 | 3 | import android.database.Cursor; 4 | import android.net.Uri; 5 | import android.os.Parcel; 6 | import android.provider.MediaStore; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.view.ViewTreeObserver; 11 | import android.widget.ImageView; 12 | 13 | import com.android.volley.toolbox.ImageLoader; 14 | 15 | import org.wordpress.mediapicker.MediaItem; 16 | import org.wordpress.mediapicker.MediaUtils; 17 | import org.wordpress.mediapicker.R; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | public class MediaSourceDeviceVideos extends MediaSourceDeviceImages { 24 | private static final String[] VIDEO_QUERY_COLUMNS = { 25 | MediaStore.Video.Media._ID, 26 | MediaStore.Video.Media.DATA }; 27 | private static final String[] THUMBNAIL_QUERY_COLUMNS = { 28 | MediaStore.Video.Media._ID, 29 | MediaStore.Video.Media.DATA, 30 | MediaStore.Video.Media.DATE_TAKEN }; 31 | 32 | @Override 33 | protected List createMediaItems() { 34 | Cursor thumbnailCursor = MediaUtils.getDeviceMediaStoreVideos(mContext.getContentResolver(), 35 | THUMBNAIL_QUERY_COLUMNS); 36 | Map thumbnailData = MediaUtils.getMediaStoreThumbnailData(thumbnailCursor, 37 | MediaStore.Video.Media.DATA, 38 | MediaStore.Video.Media._ID); 39 | 40 | return MediaUtils.createMediaItems(thumbnailData, 41 | MediaStore.Images.Media.query(mContext.getContentResolver(), 42 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 43 | VIDEO_QUERY_COLUMNS, null, null, 44 | MediaStore.MediaColumns.DATE_MODIFIED + " DESC"), 45 | MediaUtils.BackgroundFetchThumbnail.TYPE_VIDEO); 46 | } 47 | 48 | @Override 49 | public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, final ImageLoader.ImageCache cache) { 50 | if (convertView == null) { 51 | convertView = inflater.inflate(R.layout.media_item_video, parent, false); 52 | } 53 | 54 | if (convertView != null && position < mMediaItems.size()) { 55 | final MediaItem mediaItem = mMediaItems.get(position); 56 | final Uri imageSource = mediaItem.getPreviewSource(); 57 | 58 | final ImageView imageView = (ImageView) convertView.findViewById(R.id.video_view_background); 59 | if (imageView != null) { 60 | int width = imageView.getWidth(); 61 | int height = imageView.getHeight(); 62 | 63 | if (width <= 0 || height <= 0) { 64 | imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 65 | @Override 66 | public boolean onPreDraw() { 67 | int width = imageView.getWidth(); 68 | int height = imageView.getHeight(); 69 | MediaUtils.fadeMediaItemImageIntoView(imageSource, cache, imageView, mediaItem, 70 | width, height, MediaUtils.BackgroundFetchThumbnail.TYPE_VIDEO); 71 | imageView.getViewTreeObserver().removeOnPreDrawListener(this); 72 | return true; 73 | } 74 | }); 75 | } else { 76 | MediaUtils.fadeMediaItemImageIntoView(imageSource, cache, imageView, mediaItem, 77 | width, height, MediaUtils.BackgroundFetchThumbnail.TYPE_VIDEO); 78 | } 79 | } 80 | } 81 | 82 | return convertView; 83 | } 84 | 85 | /** 86 | * {@link android.os.Parcelable} interface 87 | */ 88 | 89 | public static final Creator CREATOR = 90 | new Creator() { 91 | public MediaSourceDeviceVideos createFromParcel(Parcel in) { 92 | List parcelData = new ArrayList<>(); 93 | in.readTypedList(parcelData, MediaItem.CREATOR); 94 | MediaSourceDeviceVideos newItem = new MediaSourceDeviceVideos(); 95 | 96 | if (parcelData.size() > 0) { 97 | newItem.setMediaItems(parcelData); 98 | } 99 | 100 | return newItem; 101 | } 102 | 103 | public MediaSourceDeviceVideos[] newArray(int size) { 104 | return new MediaSourceDeviceVideos[size]; 105 | } 106 | }; 107 | 108 | @Override 109 | public int describeContents() { 110 | return 0; 111 | } 112 | 113 | @Override 114 | public void writeToParcel(Parcel dest, int flags) { 115 | dest.writeTypedList(mMediaItems); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/source/CameraPreview.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker.source; 2 | 3 | import android.content.Context; 4 | import android.hardware.Camera; 5 | import android.support.annotation.NonNull; 6 | import android.util.AttributeSet; 7 | import android.util.Log; 8 | import android.view.MotionEvent; 9 | import android.view.SurfaceHolder; 10 | import android.view.SurfaceView; 11 | 12 | import java.io.IOException; 13 | import java.util.List; 14 | 15 | public class CameraPreview extends SurfaceView 16 | implements SurfaceHolder.Callback, Camera.PictureCallback { 17 | private static final String TAG = CameraPreview.class.getName(); 18 | private static final int INVALID_CAMERA_ID = -1; 19 | 20 | private int mCameraId; 21 | private Camera mCamera; 22 | private boolean mResolution; 23 | private boolean mCanTakePicture; 24 | private boolean mTakingPicture; 25 | private Camera.ShutterCallback mShutterCallback; 26 | private Camera.PictureCallback mRawCallback; 27 | private Camera.PictureCallback mJpegCallback; 28 | 29 | public CameraPreview(Context context) { 30 | super(context); 31 | 32 | mCameraId = INVALID_CAMERA_ID; 33 | getHolder().addCallback(this); 34 | } 35 | 36 | public CameraPreview(Context context, AttributeSet attrs) { 37 | super(context, attrs); 38 | 39 | mCameraId = INVALID_CAMERA_ID; 40 | getHolder().addCallback(this); 41 | } 42 | 43 | @Override 44 | public boolean onTouchEvent(@NonNull MotionEvent event) { 45 | if (mCanTakePicture && mCamera != null && !mTakingPicture) { 46 | mTakingPicture = true; 47 | mCamera.takePicture(mShutterCallback, mRawCallback, this); 48 | 49 | return true; 50 | } 51 | 52 | return false; 53 | } 54 | 55 | @Override 56 | public void surfaceCreated(SurfaceHolder holder) { 57 | // surfaceChanged is called immediately after, no need to duplicate code 58 | } 59 | 60 | @Override 61 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 62 | if (holder.getSurface() != null) { 63 | stopCameraPreview(); 64 | prepareCamera(); 65 | startCameraPreview(holder); 66 | } 67 | } 68 | 69 | @Override 70 | public void surfaceDestroyed(SurfaceHolder holder) { 71 | stopCameraPreview(); 72 | releaseCamera(); 73 | } 74 | 75 | @Override 76 | public void onPictureTaken(byte[] data, Camera camera) { 77 | if (mJpegCallback != null) { 78 | mJpegCallback.onPictureTaken(data, camera); 79 | 80 | mTakingPicture = false; 81 | } 82 | } 83 | 84 | public void setCanTakePicture(boolean canTake) { 85 | mCanTakePicture = canTake; 86 | } 87 | 88 | public void setCameraCallbacks(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg) { 89 | mShutterCallback = shutter; 90 | mRawCallback = raw; 91 | mJpegCallback = jpeg; 92 | } 93 | 94 | public void setHighRes(boolean highRes) { 95 | mResolution = highRes; 96 | } 97 | 98 | public Camera getCamera() { 99 | return mCamera; 100 | } 101 | 102 | public int getCameraId() { 103 | return mCameraId; 104 | } 105 | 106 | public void setCamera(int id) { 107 | if (mCamera == null && mCameraId == INVALID_CAMERA_ID) { 108 | try { 109 | mCameraId = id; 110 | mCamera = Camera.open(id); 111 | } catch (RuntimeException openException) { 112 | mCameraId = INVALID_CAMERA_ID; 113 | Log.w(TAG, "Camera failed to open: " + mCameraId); 114 | } 115 | } 116 | } 117 | 118 | public void prepareCamera() { 119 | if (mCamera == null) { 120 | return; 121 | } 122 | 123 | Camera.Parameters parameters = mCamera.getParameters(); 124 | List localSizes = parameters.getSupportedPreviewSizes(); 125 | Camera.Size size = localSizes.get(mResolution ? 0 : localSizes.size() - 1); 126 | parameters.setPreviewSize(size.width, size.height); 127 | mCamera.setParameters(parameters); 128 | mCamera.setDisplayOrientation(90); 129 | } 130 | 131 | public void releaseCamera() { 132 | if (mCamera != null) { 133 | stopCameraPreview(); 134 | mCamera.release(); 135 | mCamera = null; 136 | mCameraId = INVALID_CAMERA_ID; 137 | } 138 | } 139 | 140 | public void startCameraPreview(SurfaceHolder holder) { 141 | if (mCamera != null) { 142 | try { 143 | mCamera.setPreviewDisplay(holder); 144 | mCamera.startPreview(); 145 | } catch (IOException e) { 146 | Log.w(TAG, "Failed to start camera preview"); 147 | } 148 | } 149 | } 150 | 151 | public void stopCameraPreview() { 152 | if (mCamera != null) { 153 | try { 154 | mCamera.stopPreview(); 155 | } catch (Exception e) { 156 | Log.w(TAG, "Failure stopping preview:", e); 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/MediaSourceAdapter.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.BaseAdapter; 8 | 9 | import com.android.volley.toolbox.ImageLoader; 10 | 11 | import org.wordpress.mediapicker.source.MediaSource; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * An {@link android.widget.Adapter} that creates {@link android.view.View}'s for 17 | * {@link org.wordpress.mediapicker.MediaItem}'s provided by 18 | * {@link org.wordpress.mediapicker.source.MediaSource}'s. 19 | * 20 | * The following resources are provided for convenience to modify the default layout: 21 | *
    22 | *
  • @drawable/camera : overlay image for image capture sources
  • 23 | *
  • @drawable/video : overlay image for video capture sources
  • 24 | *
  • @drawable/media_item_frame_selector : defines frame background based on check state
  • 25 | *
  • @dimen/media_item_height : height of the entire MediaItem view
  • 26 | *
  • @dimen/media_item_frame_margin_* : frame margins; * = left/top/right/bottom
  • 27 | *
  • @dimen/media_item_frame_padding_* : frame padding; * = left/top/right/bottom
  • 28 | *
29 | */ 30 | 31 | public class MediaSourceAdapter extends BaseAdapter { 32 | private final Context mContext; 33 | private final ImageLoader.ImageCache mImageCache; 34 | private final List mMediaSources; 35 | 36 | public MediaSourceAdapter(Context context, List sources, ImageLoader.ImageCache imageCache) { 37 | mMediaSources = sources; 38 | mContext = context; 39 | mImageCache = imageCache; 40 | } 41 | 42 | public void gatherFromSources(final MediaSource.OnMediaChange listener) { 43 | for (MediaSource source : mMediaSources) { 44 | if (source != null) { 45 | source.setListener(listener); 46 | source.gather(mContext); 47 | } 48 | } 49 | } 50 | 51 | @Override 52 | public int getViewTypeCount() { 53 | return mMediaSources.size() <= 0 ? 1 : mMediaSources.size(); 54 | } 55 | 56 | @Override 57 | public int getCount() { 58 | return totalItems(); 59 | } 60 | 61 | @Override 62 | public int getItemViewType(int position) { 63 | return mMediaSources.indexOf(sourceAtPosition(position)); 64 | } 65 | 66 | @Override 67 | public MediaItem getItem(int position) { 68 | MediaSource sourceAtPosition = sourceAtPosition(position); 69 | 70 | return sourceAtPosition != null ? sourceAtPosition.getMedia(position - offsetAtPosition(position)) : null; 71 | } 72 | 73 | @Override 74 | public long getItemId(int position) { 75 | return position; 76 | } 77 | 78 | @Override 79 | public View getView(int position, View convertView, ViewGroup parent) { 80 | MediaSource itemSource = sourceAtPosition(position); 81 | 82 | if (itemSource != null) { 83 | return itemSource.getView(position - offsetAtPosition(position), convertView, parent, LayoutInflater.from(mContext), mImageCache); 84 | } 85 | 86 | return null; 87 | } 88 | 89 | /** 90 | * Helper method; determines the total number of items in all MediaSources 91 | * 92 | * @return 93 | * the total number of MediaItems contained in the MediaSource 94 | */ 95 | private int totalItems() { 96 | int count = 0; 97 | for (int i = 0; i < mMediaSources.size(); ++i) { 98 | MediaSource source = mMediaSources.get(i); 99 | count += source != null ? source.getCount() : 0; 100 | } 101 | 102 | return count; 103 | } 104 | 105 | /** 106 | * Determines the MediaSource that contains the MediaItem at the specified position. 107 | * 108 | * @param position 109 | * the absolute position in the list of all MediaItems 110 | * @return 111 | * the parent MediaSource for the MediaItem at the specified position 112 | */ 113 | public MediaSource sourceAtPosition(int position) { 114 | int count = 0; 115 | for (int i = 0; i < mMediaSources.size(); ++i) { 116 | MediaSource source = mMediaSources.get(i); 117 | count += source != null ? source.getCount() : 0; 118 | 119 | if (position < count) { 120 | return mMediaSources.get(i); 121 | } 122 | } 123 | 124 | return null; 125 | } 126 | 127 | /** 128 | * Helper method; determines offset into individual MediaSource 129 | * 130 | * @param position 131 | * position of item within all MediaSources 132 | * @return 133 | * position of item within its parent MediaSource 134 | */ 135 | private int offsetAtPosition(int position) { 136 | int offset = 0; 137 | for (int i = 0; i < mMediaSources.size(); ++i) { 138 | if (position < (offset + mMediaSources.get(i).getCount())) { 139 | return offset; 140 | } 141 | 142 | MediaSource source = mMediaSources.get(i); 143 | offset += source != null ? source.getCount() : 0; 144 | } 145 | 146 | return offset; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /sample/src/main/java/org/wordpress/mediapickersample/SampleActivity.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapickersample; 2 | 3 | import android.app.Fragment; 4 | import android.app.FragmentManager; 5 | import android.app.ActionBar; 6 | import android.os.Bundle; 7 | 8 | import android.support.v13.app.FragmentPagerAdapter; 9 | import android.support.v4.view.ViewPager; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.view.MenuItem; 12 | import android.widget.Toast; 13 | 14 | import com.android.volley.toolbox.ImageLoader; 15 | 16 | import org.wordpress.mediapicker.MediaItem; 17 | import org.wordpress.mediapicker.MediaPickerFragment; 18 | import org.wordpress.mediapicker.source.MediaSource; 19 | import org.wordpress.mediapicker.source.MediaSourceDeviceImages; 20 | import org.wordpress.mediapicker.source.MediaSourceDeviceVideos; 21 | import org.wordpress.mediapickersample.source.MediaSourceError; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | /** 27 | * Demonstrates the intended usage of the MediaPicker-Android library. 28 | */ 29 | 30 | public class SampleActivity extends AppCompatActivity 31 | implements MediaPickerFragment.OnMediaSelected { 32 | private static final String TAB_TITLE_IMAGES = "Images"; 33 | private static final String TAB_TITLE_VIDEOS = "Videos"; 34 | private static final String TAB_TITLE_EMPTY = "Empty"; 35 | private static final String TAB_TITLE_ERROR = "Error"; 36 | 37 | private MediaPickerAdapter mMediaPickerAdapter; 38 | private SlidingTabLayout mTabLayout; 39 | private ViewPager mViewPager; 40 | 41 | @Override 42 | public void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | 45 | initializeContentView(); 46 | } 47 | 48 | private void initializeContentView() { 49 | setContentView(R.layout.activity_media_picker_sample); 50 | 51 | mMediaPickerAdapter = new MediaPickerAdapter(getFragmentManager()); 52 | 53 | mTabLayout = (SlidingTabLayout) findViewById(R.id.media_picker_tabs); 54 | mViewPager = (ViewPager) findViewById(R.id.media_picker_pager); 55 | 56 | if (mViewPager != null) { 57 | initializeTabs(); 58 | mViewPager.setAdapter(mMediaPickerAdapter); 59 | 60 | if (mTabLayout != null) { 61 | mTabLayout.setViewPager(mViewPager); 62 | } 63 | } 64 | 65 | final ActionBar actionBar = getActionBar(); 66 | if (actionBar != null) { 67 | actionBar.setDisplayShowTitleEnabled(true); 68 | actionBar.setHomeButtonEnabled(true); 69 | } 70 | } 71 | 72 | /** 73 | * Adds tabs to the adapter. 74 | */ 75 | private void initializeTabs() { 76 | ArrayList imageSources = new ArrayList<>(); 77 | imageSources.add(new MediaSourceDeviceImages()); 78 | mMediaPickerAdapter.addTab(imageSources, TAB_TITLE_IMAGES); 79 | 80 | ArrayList videoSources = new ArrayList<>(); 81 | videoSources.add(new MediaSourceDeviceVideos()); 82 | mMediaPickerAdapter.addTab(videoSources, TAB_TITLE_VIDEOS); 83 | 84 | ArrayList emptySources = new ArrayList<>(); 85 | mMediaPickerAdapter.addTab(emptySources, TAB_TITLE_EMPTY); 86 | 87 | ArrayList errorSources = new ArrayList<>(); 88 | errorSources.add(new MediaSourceError()); 89 | mMediaPickerAdapter.addTab(errorSources, TAB_TITLE_ERROR); 90 | } 91 | 92 | /* 93 | OnMediaSelected interface 94 | */ 95 | 96 | @Override 97 | public void onMediaSelectionStarted() { 98 | Toast.makeText(this, "Selection started", Toast.LENGTH_SHORT).show(); 99 | } 100 | 101 | @Override 102 | public void onMediaSelected(MediaItem mediaContent, boolean selected) { 103 | } 104 | 105 | @Override 106 | public void onMediaSelectionConfirmed(ArrayList mediaContent) { 107 | Toast.makeText(this, "Selected " + mediaContent.size() + " media item(s)", Toast.LENGTH_SHORT).show(); 108 | } 109 | 110 | @Override 111 | public void onMediaSelectionCancelled() { 112 | Toast.makeText(this, "Selection cancelled", Toast.LENGTH_SHORT).show(); 113 | } 114 | 115 | @Override 116 | public boolean onMenuItemSelected(MenuItem menuItem, ArrayList selectedContent) { 117 | if (menuItem.getItemId() == R.id.menu_media_content_selection_gallery) { 118 | Toast.makeText(this, "Gallery creation requested", Toast.LENGTH_SHORT).show(); 119 | 120 | return true; 121 | } 122 | 123 | return false; 124 | } 125 | 126 | @Override 127 | public ImageLoader.ImageCache getImageCache() { 128 | // TODO 129 | return null; 130 | } 131 | 132 | /** 133 | * Shows {@link org.wordpress.mediapicker.MediaPickerFragment}'s in a tabbed layout. 134 | */ 135 | public class MediaPickerAdapter extends FragmentPagerAdapter { 136 | private class MediaPicker { 137 | public String pickerTitle; 138 | public ArrayList mediaSources; 139 | 140 | public MediaPicker(String name, ArrayList sources) { 141 | pickerTitle = name; 142 | mediaSources = sources; 143 | } 144 | } 145 | 146 | private List mMediaPickers; 147 | 148 | private MediaPickerAdapter(FragmentManager fragmentManager) { 149 | super(fragmentManager); 150 | 151 | mMediaPickers = new ArrayList<>(); 152 | } 153 | 154 | @Override 155 | public Fragment getItem(int position) { 156 | if (position < mMediaPickers.size()) { 157 | MediaPicker mediaPicker = mMediaPickers.get(position); 158 | MediaPickerFragment fragment = new MediaPickerFragment(); 159 | if (position == 0) { 160 | fragment.setCustomLayout(R.layout.media_fragment_three_grid); 161 | } else if (position == 1) { 162 | fragment.setCustomLayout(R.layout.media_fragment_four_grid); 163 | } 164 | fragment.setActionModeMenu(R.menu.menu_media_picker_action_mode); 165 | fragment.setMediaSources(mediaPicker.mediaSources); 166 | 167 | return fragment; 168 | } 169 | 170 | return null; 171 | } 172 | 173 | @Override 174 | public int getCount() { 175 | return mMediaPickers.size(); 176 | } 177 | 178 | @Override 179 | public CharSequence getPageTitle(int position) { 180 | return mMediaPickers.get(position).pickerTitle; 181 | } 182 | 183 | public void addTab(ArrayList mediaSources, String tabName) { 184 | mMediaPickers.add(new MediaPicker(tabName, mediaSources)); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/MediaItem.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker; 2 | 3 | import android.net.Uri; 4 | import android.os.Parcel; 5 | import android.os.Parcelable; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * Simple data model to describe media information. Contains the following data: 12 | * - Tag: Used to store an arbitrary String for implementation specific use and is therefore not 13 | * used by {@link org.wordpress.mediapicker.MediaPickerFragment} to determine behavior. 14 | * - Content Title: Title of the media content. 15 | * - Preview Source: URI to media content preview. 16 | * - Content Source: Direct URI to the media content. 17 | * - Rotation: Specifies the orientation that the media was captured in. 18 | * 19 | * Implements {@link android.os.Parcelable} to allow passing via {@link android.content.Intent}'s. 20 | */ 21 | 22 | public class MediaItem implements Parcelable { 23 | private String mTag; 24 | private String mContentTitle; 25 | private Uri mContentPreviewSource; 26 | private Uri mContentSource; 27 | private int mRotation; 28 | 29 | /** 30 | * @param tag 31 | * value to set; not filtered 32 | */ 33 | public void setTag(final String tag) { 34 | mTag = tag; 35 | } 36 | 37 | /** 38 | * @return 39 | * current Tag value; may be null 40 | */ 41 | public String getTag() { 42 | return mTag; 43 | } 44 | 45 | /** 46 | * @param title 47 | * value to set; not filtered 48 | */ 49 | public void setTitle(final String title) { 50 | mContentTitle = title; 51 | } 52 | 53 | /** 54 | * @return 55 | * current Title value; may be null 56 | */ 57 | public String getTitle() { 58 | return mContentTitle; 59 | } 60 | 61 | /** 62 | * @param source 63 | * value to set 64 | */ 65 | public void setPreviewSource(final Uri source) { 66 | mContentPreviewSource = source; 67 | } 68 | 69 | /** 70 | * @param source 71 | * value to {@link android.net.Uri#parse(String)} into the preview {@link android.net.Uri} 72 | */ 73 | public void setPreviewSource(final String source) { 74 | mContentPreviewSource = Uri.parse(source); 75 | } 76 | 77 | /** 78 | * @return 79 | * current Preview source value; may be null 80 | */ 81 | public Uri getPreviewSource() { 82 | return mContentPreviewSource; 83 | } 84 | 85 | /** 86 | * @param source 87 | * value to set 88 | */ 89 | 90 | public void setSource(final Uri source) { 91 | mContentSource = source; 92 | } 93 | 94 | /** 95 | * @param source 96 | * value to {@link android.net.Uri#parse(String)} into the content {@link android.net.Uri} 97 | */ 98 | public void setSource(final String source) { 99 | mContentSource = Uri.parse(source); 100 | } 101 | 102 | /** 103 | * @return 104 | * current Preview source value; may be null 105 | */ 106 | public Uri getSource() { 107 | return mContentSource; 108 | } 109 | 110 | /** 111 | * @param rotation 112 | * value to set; not filtered 113 | */ 114 | public void setRotation(int rotation) { 115 | mRotation = rotation; 116 | } 117 | 118 | /** 119 | * @return 120 | * current Rotation value; defaults to 0 121 | */ 122 | public int getRotation() { 123 | return mRotation; 124 | } 125 | 126 | /* 127 | Parcelable interface 128 | */ 129 | public static final String PARCEL_KEY_TAG = "tag"; 130 | public static final String PARCEL_KEY_TITLE = "title"; 131 | public static final String PARCEL_KEY_PREVIEW = "preview"; 132 | public static final String PARCEL_KEY_SOURCE = "source"; 133 | public static final String PARCEL_KEY_ROTATION = "rotation"; 134 | 135 | public static final Creator CREATOR = 136 | new Creator() { 137 | public MediaItem createFromParcel(Parcel in) { 138 | List parcelData = new ArrayList<>(); 139 | in.readStringList(parcelData); 140 | 141 | if (parcelData.size() > 0) { 142 | MediaItem newItem = new MediaItem(); 143 | 144 | while (parcelData.size() > 0) { 145 | String data = parcelData.remove(0); 146 | if (data == null) continue; 147 | 148 | String key = data.substring(0, data.indexOf('=')); 149 | String value = data.substring(data.indexOf('=') + 1, data.length()); 150 | 151 | if (!key.isEmpty()) { 152 | switch (key) { 153 | case PARCEL_KEY_TAG: 154 | newItem.setTag(value); 155 | break; 156 | case PARCEL_KEY_TITLE: 157 | newItem.setTitle(value); 158 | break; 159 | case PARCEL_KEY_PREVIEW: 160 | newItem.setPreviewSource(value); 161 | break; 162 | case PARCEL_KEY_SOURCE: 163 | newItem.setSource(value); 164 | break; 165 | case PARCEL_KEY_ROTATION: 166 | newItem.setRotation(Integer.parseInt(value)); 167 | break; 168 | } 169 | } 170 | } 171 | 172 | return newItem; 173 | } 174 | 175 | return null; 176 | } 177 | 178 | public MediaItem[] newArray(int size) { 179 | return new MediaItem[size]; 180 | } 181 | }; 182 | 183 | @Override 184 | public int describeContents() { 185 | return 0; 186 | } 187 | 188 | @Override 189 | public void writeToParcel(Parcel dest, int flags) { 190 | List dataList = new ArrayList<>(); 191 | 192 | dataList.add(PARCEL_KEY_ROTATION + "=" + mRotation); 193 | if (mTag != null && !mTag.isEmpty()) { 194 | dataList.add(PARCEL_KEY_TAG + "=" + mTag); 195 | } 196 | if (mContentTitle != null && !mContentTitle.isEmpty()) { 197 | dataList.add(PARCEL_KEY_TITLE + "=" + mContentTitle); 198 | } 199 | if (mContentPreviewSource != null && !mContentPreviewSource.toString().isEmpty()) { 200 | dataList.add(PARCEL_KEY_PREVIEW + "=" + mContentPreviewSource.toString()); 201 | } 202 | if (mContentSource != null && !mContentSource.toString().isEmpty()) { 203 | dataList.add(PARCEL_KEY_SOURCE + "=" + mContentSource.toString()); 204 | } 205 | 206 | dest.writeStringList(dataList); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /mediapicker/src/androidTest/java/org/wordpress/mediapicker/MediaPickerFragmentTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.os.Parcel; 6 | import android.support.v4.app.FragmentActivity; 7 | import android.view.ActionMode; 8 | import android.view.LayoutInflater; 9 | import android.view.Menu; 10 | import android.view.MenuItem; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | 14 | import com.android.volley.toolbox.ImageLoader; 15 | 16 | import org.junit.After; 17 | import org.junit.Assert; 18 | import org.junit.Before; 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | 22 | import org.robolectric.Robolectric; 23 | import org.robolectric.RobolectricTestRunner; 24 | import org.robolectric.annotation.Config; 25 | import org.robolectric.util.FragmentTestUtil; 26 | import org.wordpress.mediapicker.source.MediaSource; 27 | 28 | import java.util.ArrayList; 29 | 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.when; 32 | 33 | @Config(emulateSdk = 18) 34 | @RunWith(RobolectricTestRunner.class) 35 | public class MediaPickerFragmentTest { 36 | private boolean tSelectionStarted; 37 | private boolean tSelectionCancelled; 38 | private boolean tSelectionConfirmed; 39 | 40 | @Before 41 | public void setUp() { 42 | tSelectionStarted = false; 43 | tSelectionCancelled = false; 44 | tSelectionConfirmed = false; 45 | } 46 | 47 | /** 48 | * Verifies that the listener is notified when media selection begins. 49 | */ 50 | @Test 51 | public void testMediaSelectionStart() { 52 | final MediaPickerFragment testFragment = new MediaPickerFragment(); 53 | final ActionMode mockActionMode = mock(ActionMode.class); 54 | final Menu mockMenu = mock(Menu.class); 55 | final MediaActivity testListener = new MediaActivity(); 56 | 57 | FragmentTestUtil.startFragment(testFragment); 58 | testFragment.setListener(testListener); 59 | testFragment.onPrepareActionMode(mockActionMode, mockMenu); 60 | 61 | Assert.assertTrue(tSelectionStarted); 62 | } 63 | 64 | /** 65 | * Verifies that the listener is notified when media selection is cancelled. 66 | */ 67 | @Test 68 | public void testMediaSelectionCancelled() { 69 | final MediaPickerFragment testFragment = new MediaPickerFragment(); 70 | final ActionMode mockActionMode = mock(ActionMode.class); 71 | final MediaActivity testListener = new MediaActivity(); 72 | 73 | FragmentTestUtil.startFragment(testFragment); 74 | testFragment.setListener(testListener); 75 | testFragment.onDestroyActionMode(mockActionMode); 76 | 77 | Assert.assertTrue(tSelectionCancelled); 78 | } 79 | 80 | /** 81 | * Verifies that a single item being clicked confirms selection. 82 | */ 83 | @Test 84 | public void testMediaSelectionConfirmedFromSingleClick() { 85 | final MediaPickerFragment testFragment = new MediaPickerFragment(); 86 | final MediaActivity testListener = new MediaActivity(); 87 | final ArrayList testMediaSources = new ArrayList<>(); 88 | testMediaSources.add(mMediaSourceOnMediaSelectedFalse); 89 | final MediaSourceAdapter mockAdapter = mock(MediaSourceAdapter.class); 90 | 91 | when(mockAdapter.getItem(0)).thenReturn(new MediaItem()); 92 | when(mockAdapter.getCount()).thenReturn(1); 93 | when(mockAdapter.getViewTypeCount()).thenReturn(1); 94 | when(mockAdapter.sourceAtPosition(0)).thenReturn(mMediaSourceOnMediaSelectedFalse); 95 | 96 | FragmentTestUtil.startFragment(testFragment); 97 | testFragment.setListener(testListener); 98 | testFragment.setMediaSources(testMediaSources); 99 | testFragment.setAdapter(mockAdapter); 100 | testFragment.onItemClick(null, null, 0, 0); 101 | 102 | Assert.assertTrue(tSelectionConfirmed); 103 | } 104 | 105 | /** 106 | * Verifies that selecting the confirm option in Action Mode confirms selection. 107 | */ 108 | @Test 109 | public void testMediaSelectionConfirmedFromActionModeMenu() { 110 | final MediaPickerFragment testFragment = new MediaPickerFragment(); 111 | final ActionMode mockActionMode = mock(ActionMode.class); 112 | final MenuItem mockMenuItem = mock(MenuItem.class); 113 | final MediaActivity testListener = new MediaActivity(); 114 | 115 | when(mockMenuItem.getItemId()).thenReturn(R.id.menu_media_selection_confirmed); 116 | 117 | FragmentTestUtil.startFragment(testFragment); 118 | testFragment.setListener(testListener); 119 | testFragment.onItemCheckedStateChanged(mockActionMode, 0, 0, true); 120 | testFragment.onActionItemClicked(mockActionMode, mockMenuItem); 121 | 122 | Assert.assertTrue(tSelectionConfirmed); 123 | } 124 | 125 | private MediaSource mMediaSourceOnMediaSelectedFalse = new MediaSource() { 126 | @Override 127 | public void gather(Context context) { 128 | } 129 | 130 | @Override 131 | public void cleanup() { 132 | } 133 | 134 | @Override 135 | public void setListener(OnMediaChange listener) { 136 | } 137 | 138 | @Override 139 | public int getCount() { 140 | return 1; 141 | } 142 | 143 | @Override 144 | public MediaItem getMedia(int position) { 145 | return null; 146 | } 147 | 148 | @Override 149 | public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, ImageLoader.ImageCache cache) { 150 | return null; 151 | } 152 | 153 | @Override 154 | public boolean onMediaItemSelected(MediaItem mediaItem, boolean selected) { 155 | return false; 156 | } 157 | 158 | @Override 159 | public int describeContents() { 160 | return 0; 161 | } 162 | 163 | @Override 164 | public void writeToParcel(Parcel dest, int flags) { 165 | } 166 | }; 167 | 168 | /** 169 | * Dummy class for testing various media selection interface calls. 170 | */ 171 | public class MediaActivity extends Activity implements MediaPickerFragment.OnMediaSelected { 172 | @Override 173 | public void onMediaSelectionStarted() { 174 | tSelectionStarted = true; 175 | } 176 | 177 | @Override 178 | public void onMediaSelected(MediaItem mediaContent, boolean selected) { 179 | } 180 | 181 | @Override 182 | public void onMediaSelectionConfirmed(ArrayList mediaContent) { 183 | tSelectionConfirmed = true; 184 | } 185 | 186 | @Override 187 | public void onMediaSelectionCancelled() { 188 | tSelectionCancelled = true; 189 | } 190 | 191 | @Override 192 | public boolean onMenuItemSelected(MenuItem menuItem, ArrayList selectedContent) { 193 | return false; 194 | } 195 | 196 | @Override 197 | public ImageLoader.ImageCache getImageCache() { 198 | return null; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /sample/src/main/java/org/wordpress/mediapickersample/SlidingTabStrip.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.wordpress.mediapickersample; 18 | 19 | import android.R; 20 | import android.content.Context; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.Paint; 24 | import android.util.AttributeSet; 25 | import android.util.TypedValue; 26 | import android.view.View; 27 | import android.widget.LinearLayout; 28 | 29 | class SlidingTabStrip extends LinearLayout { 30 | 31 | private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2; 32 | private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; 33 | private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8; 34 | private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; 35 | 36 | private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1; 37 | private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20; 38 | private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f; 39 | 40 | private final int mBottomBorderThickness; 41 | private final Paint mBottomBorderPaint; 42 | 43 | private final int mSelectedIndicatorThickness; 44 | private final Paint mSelectedIndicatorPaint; 45 | 46 | private final int mDefaultBottomBorderColor; 47 | 48 | private final Paint mDividerPaint; 49 | private final float mDividerHeight; 50 | 51 | private int mSelectedPosition; 52 | private float mSelectionOffset; 53 | 54 | private SlidingTabLayout.TabColorizer mCustomTabColorizer; 55 | private final SimpleTabColorizer mDefaultTabColorizer; 56 | 57 | SlidingTabStrip(Context context) { 58 | this(context, null); 59 | } 60 | 61 | SlidingTabStrip(Context context, AttributeSet attrs) { 62 | super(context, attrs); 63 | setWillNotDraw(false); 64 | 65 | final float density = getResources().getDisplayMetrics().density; 66 | 67 | TypedValue outValue = new TypedValue(); 68 | context.getTheme().resolveAttribute(R.attr.colorForeground, outValue, true); 69 | final int themeForegroundColor = outValue.data; 70 | 71 | mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, 72 | DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); 73 | 74 | mDefaultTabColorizer = new SimpleTabColorizer(); 75 | mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); 76 | mDefaultTabColorizer.setDividerColors(setColorAlpha(themeForegroundColor, 77 | DEFAULT_DIVIDER_COLOR_ALPHA)); 78 | 79 | mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); 80 | mBottomBorderPaint = new Paint(); 81 | mBottomBorderPaint.setColor(mDefaultBottomBorderColor); 82 | 83 | mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); 84 | mSelectedIndicatorPaint = new Paint(); 85 | 86 | mDividerHeight = DEFAULT_DIVIDER_HEIGHT; 87 | mDividerPaint = new Paint(); 88 | mDividerPaint.setStrokeWidth((int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density)); 89 | } 90 | 91 | void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { 92 | mCustomTabColorizer = customTabColorizer; 93 | invalidate(); 94 | } 95 | 96 | void setSelectedIndicatorColors(int... colors) { 97 | // Make sure that the custom colorizer is removed 98 | mCustomTabColorizer = null; 99 | mDefaultTabColorizer.setIndicatorColors(colors); 100 | invalidate(); 101 | } 102 | 103 | void setDividerColors(int... colors) { 104 | // Make sure that the custom colorizer is removed 105 | mCustomTabColorizer = null; 106 | mDefaultTabColorizer.setDividerColors(colors); 107 | invalidate(); 108 | } 109 | 110 | void onViewPagerPageChanged(int position, float positionOffset) { 111 | mSelectedPosition = position; 112 | mSelectionOffset = positionOffset; 113 | invalidate(); 114 | } 115 | 116 | @Override 117 | protected void onDraw(Canvas canvas) { 118 | final int height = getHeight(); 119 | final int childCount = getChildCount(); 120 | final int dividerHeightPx = (int) (Math.min(Math.max(0f, mDividerHeight), 1f) * height); 121 | final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null 122 | ? mCustomTabColorizer 123 | : mDefaultTabColorizer; 124 | 125 | // Thick colored underline below the current selection 126 | if (childCount > 0) { 127 | View selectedTitle = getChildAt(mSelectedPosition); 128 | int left = selectedTitle.getLeft(); 129 | int right = selectedTitle.getRight(); 130 | int color = tabColorizer.getIndicatorColor(mSelectedPosition); 131 | 132 | if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { 133 | int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); 134 | if (color != nextColor) { 135 | color = blendColors(nextColor, color, mSelectionOffset); 136 | } 137 | 138 | // Draw the selection partway between the tabs 139 | View nextTitle = getChildAt(mSelectedPosition + 1); 140 | left = (int) (mSelectionOffset * nextTitle.getLeft() + 141 | (1.0f - mSelectionOffset) * left); 142 | right = (int) (mSelectionOffset * nextTitle.getRight() + 143 | (1.0f - mSelectionOffset) * right); 144 | } 145 | 146 | mSelectedIndicatorPaint.setColor(color); 147 | 148 | canvas.drawRect(left, height - mSelectedIndicatorThickness, right, 149 | height, mSelectedIndicatorPaint); 150 | } 151 | 152 | // Thin underline along the entire bottom edge 153 | canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); 154 | 155 | // Vertical separators between the titles 156 | int separatorTop = (height - dividerHeightPx) / 2; 157 | for (int i = 0; i < childCount - 1; i++) { 158 | View child = getChildAt(i); 159 | mDividerPaint.setColor(tabColorizer.getDividerColor(i)); 160 | canvas.drawLine(child.getRight(), separatorTop, child.getRight(), 161 | separatorTop + dividerHeightPx, mDividerPaint); 162 | } 163 | } 164 | 165 | /** 166 | * Set the alpha value of the {@code color} to be the given {@code alpha} value. 167 | */ 168 | private static int setColorAlpha(int color, byte alpha) { 169 | return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); 170 | } 171 | 172 | /** 173 | * Blend {@code color1} and {@code color2} using the given ratio. 174 | * 175 | * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, 176 | * 0.0 will return {@code color2}. 177 | */ 178 | private static int blendColors(int color1, int color2, float ratio) { 179 | final float inverseRation = 1f - ratio; 180 | float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); 181 | float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); 182 | float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); 183 | return Color.rgb((int) r, (int) g, (int) b); 184 | } 185 | 186 | private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { 187 | private int[] mIndicatorColors; 188 | private int[] mDividerColors; 189 | 190 | @Override 191 | public final int getIndicatorColor(int position) { 192 | return mIndicatorColors[position % mIndicatorColors.length]; 193 | } 194 | 195 | @Override 196 | public final int getDividerColor(int position) { 197 | return mDividerColors[position % mDividerColors.length]; 198 | } 199 | 200 | void setIndicatorColors(int... colors) { 201 | mIndicatorColors = colors; 202 | } 203 | 204 | void setDividerColors(int... colors) { 205 | mDividerColors = colors; 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /mediapicker/src/androidTest/java/org/wordpress/mediapicker/MediaSourceAdapterTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker; 2 | 3 | import android.content.ContentResolver; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import com.android.volley.toolbox.ImageLoader; 9 | 10 | import org.junit.Assert; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | 14 | import org.robolectric.Robolectric; 15 | import org.robolectric.RobolectricTestRunner; 16 | import org.robolectric.annotation.Config; 17 | import org.wordpress.mediapicker.source.MediaSource; 18 | import org.wordpress.mediapicker.source.MediaSourceDeviceImages; 19 | import org.wordpress.mediapicker.source.MediaSourceDeviceVideos; 20 | 21 | import java.util.ArrayList; 22 | 23 | import static org.mockito.Matchers.any; 24 | import static org.mockito.Matchers.anyInt; 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.when; 27 | 28 | @Config(emulateSdk = 18) 29 | @RunWith(RobolectricTestRunner.class) 30 | public class MediaSourceAdapterTest { 31 | /** 32 | * Verifies that a view type exists even if there is no media source, for an empty view. 33 | */ 34 | @Test 35 | public void testViewTypeCountWithNoSources() { 36 | final ArrayList testSources = new ArrayList<>(); 37 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 38 | 39 | Assert.assertEquals(1, testAdapter.getViewTypeCount()); 40 | } 41 | 42 | /** 43 | * Verifies that a single source has a single view type. 44 | */ 45 | @Test 46 | public void testViewTypeCountWithOneSource() { 47 | final ArrayList testSources = new ArrayList<>(); 48 | testSources.add(new MediaSourceDeviceImages()); 49 | 50 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 51 | 52 | Assert.assertEquals(testSources.size(), testAdapter.getViewTypeCount()); 53 | } 54 | 55 | /** 56 | * Verifies there are as many view types as media sources. 57 | */ 58 | @Test 59 | public void testViewTypeCountWithManySources() { 60 | final ArrayList testSources = new ArrayList<>(); 61 | testSources.add(new MediaSourceDeviceImages()); 62 | testSources.add(new MediaSourceDeviceVideos()); 63 | 64 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 65 | 66 | Assert.assertEquals(testSources.size(), testAdapter.getViewTypeCount()); 67 | } 68 | 69 | /** 70 | * Verifies an invalid view type is returned if no MediaSources are present. 71 | */ 72 | @Test 73 | public void testViewTypeWithNoSources() { 74 | final ArrayList testSources = new ArrayList<>(); 75 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 76 | 77 | Assert.assertEquals(-1, testAdapter.getItemViewType(0)); 78 | } 79 | 80 | /** 81 | * Verifies that the view type is unique for each source. 82 | */ 83 | @Test 84 | public void testViewTypeWithManySources() { 85 | final ArrayList testSources = new ArrayList<>(); 86 | final MediaSourceDeviceImages mockSource1 = mock(MediaSourceDeviceImages.class); 87 | final MediaSourceDeviceImages mockSource2 = mock(MediaSourceDeviceImages.class); 88 | final int testItemCount = 10; 89 | 90 | when(mockSource1.getCount()).thenReturn(testItemCount); 91 | when(mockSource2.getCount()).thenReturn(testItemCount); 92 | testSources.add(mockSource1); 93 | testSources.add(mockSource2); 94 | 95 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 96 | 97 | Assert.assertEquals(1, testAdapter.getItemViewType(testItemCount + 1)); 98 | } 99 | 100 | /** 101 | * Verifies that the count can succeed with a single source. 102 | */ 103 | @Test 104 | public void testCountWithOneSource() { 105 | final ArrayList testSources = new ArrayList<>(); 106 | final MediaSourceDeviceImages mockSource = mock(MediaSourceDeviceImages.class); 107 | final int testItemCount = 10; 108 | 109 | when(mockSource.getCount()).thenReturn(testItemCount); 110 | testSources.add(mockSource); 111 | 112 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 113 | 114 | Assert.assertEquals(testItemCount, testAdapter.getCount()); 115 | } 116 | 117 | /** 118 | * Verifies that all items from all sources are included in the count. 119 | */ 120 | @Test 121 | public void testCountWithManySources() { 122 | final ArrayList testSources = new ArrayList<>(); 123 | final MediaSourceDeviceImages mockSource1 = mock(MediaSourceDeviceImages.class); 124 | final MediaSourceDeviceImages mockSource2 = mock(MediaSourceDeviceImages.class); 125 | final int testItemCount = 10; 126 | 127 | when(mockSource1.getCount()).thenReturn(testItemCount); 128 | when(mockSource2.getCount()).thenReturn(testItemCount); 129 | testSources.add(mockSource1); 130 | testSources.add(mockSource2); 131 | 132 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 133 | 134 | Assert.assertEquals(testItemCount * testSources.size(), testAdapter.getCount()); 135 | } 136 | 137 | /** 138 | * Verifies that the appropriate {@link org.wordpress.mediapicker.MediaItem} is retrieved given 139 | * a multiple number of sources. 140 | */ 141 | @Test 142 | public void testGetItem() { 143 | final ArrayList testSources = new ArrayList<>(); 144 | final MediaSourceDeviceImages mockSource1 = mock(MediaSourceDeviceImages.class); 145 | final MediaSourceDeviceImages mockSource2 = mock(MediaSourceDeviceImages.class); 146 | final int testItemCount = 10; 147 | final MediaItem testItem = new MediaItem(); 148 | 149 | when(mockSource1.getCount()).thenReturn(testItemCount); 150 | when(mockSource2.getCount()).thenReturn(testItemCount); 151 | when(mockSource2.getMedia(1)).thenReturn(testItem); 152 | testSources.add(mockSource1); 153 | testSources.add(mockSource2); 154 | 155 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 156 | final MediaItem mediaItem = testAdapter.getItem(testItemCount + 1); 157 | 158 | Assert.assertEquals(testItem, mediaItem); 159 | } 160 | 161 | /** 162 | * Verifies that the correct {@link org.wordpress.mediapicker.source.MediaSource} is used to 163 | * create a {@link android.view.View}. 164 | * 165 | * TODO: seems to me that anyInt() is inappropriate, it should be specifically the first element of mockSource2. 166 | */ 167 | @Test 168 | public void testGetView() { 169 | final ArrayList testSources = new ArrayList<>(); 170 | final MediaSourceDeviceImages mockSource1 = mock(MediaSourceDeviceImages.class); 171 | final MediaSourceDeviceImages mockSource2 = mock(MediaSourceDeviceImages.class); 172 | final int testItemCount = 10; 173 | final MediaItem testItem = new MediaItem(); 174 | final View testView = mock(View.class); 175 | 176 | when(mockSource2 177 | .getView(anyInt(), any(View.class), any(ViewGroup.class), any(LayoutInflater.class), any(ImageLoader.ImageCache.class))) 178 | .thenReturn(testView); 179 | when(mockSource1.getCount()).thenReturn(testItemCount); 180 | when(mockSource2.getCount()).thenReturn(testItemCount); 181 | when(mockSource2.getMedia(1)).thenReturn(testItem); 182 | testSources.add(mockSource1); 183 | testSources.add(mockSource2); 184 | 185 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 186 | 187 | Assert.assertEquals(testView, testAdapter.getView(testItemCount, null, null)); 188 | } 189 | 190 | /** 191 | * Verifies that each {@link org.wordpress.mediapicker.MediaItem} has a unique ID.s 192 | */ 193 | @Test 194 | public void testItemId() { 195 | final ArrayList testSources = new ArrayList<>(); 196 | final MediaSourceDeviceImages mockSource1 = mock(MediaSourceDeviceImages.class); 197 | final MediaSourceDeviceImages mockSource2 = mock(MediaSourceDeviceImages.class); 198 | final int testItemCount = 10; 199 | 200 | when(mockSource1.getCount()).thenReturn(testItemCount); 201 | when(mockSource2.getCount()).thenReturn(testItemCount); 202 | testSources.add(mockSource1); 203 | testSources.add(mockSource2); 204 | 205 | final MediaSourceAdapter testAdapter = new MediaSourceAdapter(Robolectric.application, testSources, null); 206 | 207 | for (int i = 0; i < testItemCount * testSources.size(); ++i) { 208 | Assert.assertEquals(i, testAdapter.getItemId(i)); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/source/MediaSourceDeviceImages.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker.source; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.net.Uri; 6 | import android.os.AsyncTask; 7 | import android.os.Parcel; 8 | import android.provider.MediaStore; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.view.ViewTreeObserver; 13 | import android.widget.ImageView; 14 | 15 | import com.android.volley.toolbox.ImageLoader; 16 | 17 | import org.wordpress.mediapicker.MediaItem; 18 | import org.wordpress.mediapicker.MediaUtils; 19 | import org.wordpress.mediapicker.R; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | /** 26 | * A {@link org.wordpress.mediapicker.source.MediaSource} that loads images from the device 27 | * {@link android.provider.MediaStore}. 28 | */ 29 | 30 | public class MediaSourceDeviceImages implements MediaSource { 31 | // Columns to query from the thumbnail MediaStore database 32 | private static final String[] THUMBNAIL_QUERY_COLUMNS = { 33 | MediaStore.Images.Thumbnails._ID, 34 | MediaStore.Images.Thumbnails.DATA, 35 | MediaStore.Images.Thumbnails.IMAGE_ID 36 | }; 37 | // Columns to query from the image MediaStore database 38 | private static final String[] IMAGE_QUERY_COLUMNS = { 39 | MediaStore.Images.Media._ID, 40 | MediaStore.Images.Media.DATA, 41 | MediaStore.Images.Media.DATE_TAKEN, 42 | MediaStore.Images.Media.ORIENTATION 43 | }; 44 | 45 | protected final List mMediaItems; 46 | 47 | protected Context mContext; 48 | 49 | private OnMediaChange mListener; 50 | private AsyncTask mGatheringTask; 51 | 52 | public MediaSourceDeviceImages() { 53 | mMediaItems = new ArrayList<>(); 54 | } 55 | 56 | protected List createMediaItems() { 57 | Cursor thumbnailCursor = MediaUtils.getMediaStoreThumbnails(mContext.getContentResolver(), 58 | THUMBNAIL_QUERY_COLUMNS); 59 | Map thumbnailData = MediaUtils.getMediaStoreThumbnailData(thumbnailCursor, 60 | MediaStore.Images.Thumbnails.DATA, 61 | MediaStore.Images.Thumbnails.IMAGE_ID); 62 | 63 | return MediaUtils.createMediaItems(thumbnailData, 64 | MediaStore.Images.Media.query(mContext.getContentResolver(), 65 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 66 | IMAGE_QUERY_COLUMNS, null, null, 67 | MediaStore.MediaColumns.DATE_MODIFIED + " DESC"), 68 | MediaUtils.BackgroundFetchThumbnail.TYPE_IMAGE); 69 | } 70 | 71 | @Override 72 | public void gather(Context context) { 73 | // cancel the current gather task 74 | if (mGatheringTask != null) { 75 | mGatheringTask.cancel(true); 76 | } 77 | 78 | // store reference to latest Context 79 | mContext = context; 80 | 81 | // start gathering 82 | mGatheringTask = new GatherDeviceImagesTask(); 83 | mGatheringTask.execute(); 84 | } 85 | 86 | @Override 87 | public void cleanup() { 88 | if (mGatheringTask != null) { 89 | // cancel gathering media data immediately, do not wait for the task to finish 90 | mGatheringTask.cancel(true); 91 | } 92 | mMediaItems.clear(); 93 | } 94 | 95 | @Override 96 | public void setListener(final OnMediaChange listener) { 97 | mListener = listener; 98 | } 99 | 100 | @Override 101 | public int getCount() { 102 | return mMediaItems.size(); 103 | } 104 | 105 | @Override 106 | public MediaItem getMedia(int position) { 107 | return (position < mMediaItems.size()) ? mMediaItems.get(position) : null; 108 | } 109 | 110 | @Override 111 | public View getView(int position, View convertView, ViewGroup parent, LayoutInflater inflater, final ImageLoader.ImageCache cache) { 112 | if (convertView == null) { 113 | convertView = inflater.inflate(R.layout.media_item_image, parent, false); 114 | } 115 | 116 | final MediaItem mediaItem = mMediaItems.get(position); 117 | if (convertView != null && mediaItem != null) { 118 | final ImageView imageView = (ImageView) convertView.findViewById(R.id.image_view_background); 119 | final Uri imageSource; 120 | if (mediaItem.getPreviewSource() != null && !mediaItem.getPreviewSource().toString().isEmpty()) { 121 | imageSource = mediaItem.getPreviewSource(); 122 | } else { 123 | imageSource = mediaItem.getSource(); 124 | } 125 | 126 | if (imageView != null) { 127 | int width = imageView.getWidth(); 128 | int height = imageView.getHeight(); 129 | 130 | if (width <= 0 || height <= 0) { 131 | imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 132 | @Override 133 | public boolean onPreDraw() { 134 | int width = imageView.getWidth(); 135 | int height = imageView.getHeight(); 136 | MediaUtils.fadeMediaItemImageIntoView(imageSource, cache, imageView, mediaItem, 137 | width, height, MediaUtils.BackgroundFetchThumbnail.TYPE_IMAGE); 138 | imageView.getViewTreeObserver().removeOnPreDrawListener(this); 139 | return true; 140 | } 141 | }); 142 | } else { 143 | MediaUtils.fadeMediaItemImageIntoView(imageSource, cache, imageView, mediaItem, 144 | width, height, MediaUtils.BackgroundFetchThumbnail.TYPE_IMAGE); 145 | } 146 | } 147 | } 148 | 149 | return convertView; 150 | } 151 | 152 | @Override 153 | public boolean onMediaItemSelected(MediaItem mediaItem, boolean selected) { 154 | return !selected; 155 | } 156 | 157 | /** 158 | * Clears the current media items then adds the provided items. 159 | */ 160 | protected void setMediaItems(List mediaItems) { 161 | mMediaItems.clear(); 162 | mMediaItems.addAll(mediaItems); 163 | } 164 | 165 | /** 166 | * Invokes 167 | * {@link org.wordpress.mediapicker.source.MediaSource.OnMediaChange#onMediaLoaded(boolean)} 168 | * if {@link #mListener} is not null. 169 | * 170 | * @param success 171 | * passthrough parameter 172 | */ 173 | protected void notifyMediaLoaded(boolean success) { 174 | if (mListener != null) { 175 | mListener.onMediaLoaded(success); 176 | } 177 | } 178 | 179 | /** 180 | * Gathers media items on a background thread. 181 | */ 182 | protected class GatherDeviceImagesTask extends AsyncTask { 183 | @Override 184 | protected void onPreExecute() { 185 | // delete references to any existing media items before gathering 186 | mMediaItems.clear(); 187 | } 188 | 189 | @Override 190 | protected Void doInBackground(Void... params) { 191 | mMediaItems.addAll(createMediaItems()); 192 | return null; 193 | } 194 | 195 | @Override 196 | protected void onPostExecute(Void result) { 197 | notifyMediaLoaded(true); 198 | nullGatheringReference(); 199 | } 200 | 201 | @Override 202 | protected void onCancelled(Void result) { 203 | nullGatheringReference(); 204 | } 205 | 206 | /** 207 | * Sets MediaSourceDeviceImages.this.mGatheringTask to null if it's referencing this. 208 | */ 209 | protected void nullGatheringReference() { 210 | if (mGatheringTask == this) { 211 | mGatheringTask = null; 212 | } 213 | } 214 | } 215 | 216 | /** 217 | * {@link android.os.Parcelable} interface 218 | */ 219 | 220 | public static final Creator CREATOR = 221 | new Creator() { 222 | public MediaSourceDeviceImages createFromParcel(Parcel in) { 223 | List parcelData = new ArrayList<>(); 224 | in.readTypedList(parcelData, MediaItem.CREATOR); 225 | MediaSourceDeviceImages newItem = new MediaSourceDeviceImages(); 226 | 227 | if (parcelData.size() > 0) { 228 | newItem.setMediaItems(parcelData); 229 | } 230 | 231 | return newItem; 232 | } 233 | 234 | public MediaSourceDeviceImages[] newArray(int size) { 235 | return new MediaSourceDeviceImages[size]; 236 | } 237 | }; 238 | 239 | @Override 240 | public int describeContents() { 241 | return 0; 242 | } 243 | 244 | @Override 245 | public void writeToParcel(Parcel destination, int flags) { 246 | destination.writeTypedList(mMediaItems); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /mediapicker/src/androidTest/java/org/wordpress/mediapicker/MediaItemTest.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker; 2 | 3 | import android.os.Parcel; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | import org.mockito.invocation.InvocationOnMock; 10 | import org.mockito.stubbing.Answer; 11 | import org.robolectric.RobolectricTestRunner; 12 | import org.robolectric.annotation.Config; 13 | 14 | import java.util.List; 15 | 16 | import static org.mockito.Matchers.anyListOf; 17 | import static org.mockito.Mockito.doAnswer; 18 | import static org.mockito.Mockito.mock; 19 | 20 | @Config(emulateSdk = 18) 21 | @RunWith(RobolectricTestRunner.class) 22 | public class MediaItemTest { 23 | /** 24 | * Verifies that the CREATOR correctly constructs a {@link org.wordpress.mediapicker.MediaItem} 25 | * from valid {@link android.os.Parcel} data. 26 | */ 27 | @Test 28 | public void testCreator() { 29 | final String testTitle = "test-title"; 30 | final String testTag = "test-tag"; 31 | final String testSource = "test-source"; 32 | final String testPreview = "test-preview"; 33 | final int testRotation = 180; 34 | final Parcel mockParcel = mock(Parcel.class); 35 | 36 | doAnswer(new Answer() { 37 | @Override 38 | public Object answer(InvocationOnMock invocationOnMock) throws Throwable { 39 | Object parameter = invocationOnMock.getArguments()[0]; 40 | Assert.assertTrue(parameter instanceof List); 41 | List parcelData = (List) parameter; 42 | 43 | parcelData.add(MediaItem.PARCEL_KEY_TAG + "=" + testTag); 44 | parcelData.add(MediaItem.PARCEL_KEY_TITLE + "=" + testTitle); 45 | parcelData.add(MediaItem.PARCEL_KEY_SOURCE + "=" + testSource); 46 | parcelData.add(MediaItem.PARCEL_KEY_PREVIEW + "=" + testPreview); 47 | parcelData.add(MediaItem.PARCEL_KEY_ROTATION + "=" + testRotation); 48 | 49 | return null; 50 | } 51 | }).when(mockParcel).readStringList(anyListOf(String.class)); 52 | 53 | final MediaItem testItem = MediaItem.CREATOR.createFromParcel(mockParcel); 54 | 55 | Assert.assertEquals(testTag, testItem.getTag()); 56 | Assert.assertEquals(testTitle, testItem.getTitle()); 57 | Assert.assertEquals(testSource, testItem.getSource().toString()); 58 | Assert.assertEquals(testPreview, testItem.getPreviewSource().toString()); 59 | Assert.assertEquals(testRotation, testItem.getRotation()); 60 | } 61 | 62 | /** 63 | * Verifies that a {@link java.lang.String} representation of a {@link android.net.Uri} is 64 | * parsed correctly. 65 | */ 66 | @Test 67 | public void testSourceParsing() { 68 | final MediaItem testItem = new MediaItem(); 69 | final String testValidSource = "file://test.img"; 70 | final String testInvalidSource = "invalid-source"; 71 | 72 | testItem.setPreviewSource(testValidSource); 73 | testItem.setSource(testValidSource); 74 | Assert.assertTrue("Failed to parse preview source.", testItem.getPreviewSource().isAbsolute()); 75 | Assert.assertTrue("Failed to parse source.", testItem.getSource().isAbsolute()); 76 | 77 | testItem.setPreviewSource(testInvalidSource); 78 | testItem.setSource(testInvalidSource); 79 | Assert.assertFalse(testItem.getPreviewSource().isAbsolute()); 80 | Assert.assertFalse(testItem.getSource().isAbsolute()); 81 | } 82 | 83 | /** 84 | * Verifies that the correct data is written to an outbound {@link android.os.Parcel}. 85 | */ 86 | @Test 87 | public void testWriteToParcelWithValidData() { 88 | final String testTitle = "test-title"; 89 | final String testTag = "test-tag"; 90 | final String testSource = "test-source"; 91 | final String testPreview = "test-preview"; 92 | final int testRotation = 180; 93 | 94 | final MediaItem testItem = new MediaItem(); 95 | final Parcel mockParcel = mock(Parcel.class); 96 | 97 | doAnswer(new Answer() { 98 | @Override 99 | public Void answer(InvocationOnMock invocationOnMock) throws Throwable { 100 | Object parameter = invocationOnMock.getArguments()[0]; 101 | Assert.assertTrue(parameter instanceof List); 102 | List parcelData = (List) parameter; 103 | 104 | if (parcelData.size() > 0) { 105 | while (parcelData.size() > 0) { 106 | String data = parcelData.remove(0); 107 | String key = data.substring(0, data.indexOf('=')); 108 | String value = data.substring(data.indexOf('=') + 1, data.length()); 109 | 110 | if (!key.isEmpty()) { 111 | switch (key) { 112 | case MediaItem.PARCEL_KEY_TAG: 113 | Assert.assertTrue(testTag.equals(value)); 114 | break; 115 | case MediaItem.PARCEL_KEY_TITLE: 116 | Assert.assertTrue(testTitle.equals(value)); 117 | break; 118 | case MediaItem.PARCEL_KEY_PREVIEW: 119 | Assert.assertTrue(testPreview.equals(value)); 120 | break; 121 | case MediaItem.PARCEL_KEY_SOURCE: 122 | Assert.assertTrue(testSource.equals(value)); 123 | break; 124 | case MediaItem.PARCEL_KEY_ROTATION: 125 | Assert.assertTrue(testRotation == Integer.valueOf(value)); 126 | break; 127 | } 128 | } 129 | } 130 | } 131 | 132 | return null; 133 | } 134 | }).when(mockParcel).writeStringList(anyListOf(String.class)); 135 | 136 | testItem.setTitle(testTitle); 137 | testItem.setTag(testTag); 138 | testItem.setSource(testSource); 139 | testItem.setPreviewSource(testPreview); 140 | testItem.setRotation(testRotation); 141 | 142 | testItem.writeToParcel(mockParcel, 0); 143 | } 144 | 145 | /** 146 | * Verifies that null data is not written to an outbound {@link android.os.Parcel}. 147 | */ 148 | @Test 149 | public void testWriteToParcelWithNullData() { 150 | final MediaItem testItem = new MediaItem(); 151 | final Parcel mockParcel = mock(Parcel.class); 152 | 153 | doAnswer(new Answer() { 154 | @Override 155 | public Void answer(InvocationOnMock invocationOnMock) throws Throwable { 156 | Object parameter = invocationOnMock.getArguments()[0]; 157 | Assert.assertTrue(parameter instanceof List); 158 | List parcelData = (List) parameter; 159 | 160 | if (parcelData.size() > 0) { 161 | while (parcelData.size() > 0) { 162 | String data = parcelData.remove(0); 163 | String key = data.substring(0, data.indexOf('=')); 164 | String value = data.substring(data.indexOf('=') + 1, data.length()); 165 | 166 | if (!key.isEmpty()) { 167 | switch (key) { 168 | case MediaItem.PARCEL_KEY_TAG: 169 | Assert.fail(); 170 | break; 171 | case MediaItem.PARCEL_KEY_TITLE: 172 | Assert.fail(); 173 | break; 174 | case MediaItem.PARCEL_KEY_PREVIEW: 175 | Assert.fail(); 176 | break; 177 | case MediaItem.PARCEL_KEY_SOURCE: 178 | Assert.fail(); 179 | break; 180 | case MediaItem.PARCEL_KEY_ROTATION: 181 | Assert.assertTrue(0 == Integer.valueOf(value)); 182 | break; 183 | } 184 | } 185 | } 186 | } 187 | 188 | return null; 189 | } 190 | }).when(mockParcel).writeStringList(anyListOf(String.class)); 191 | 192 | testItem.writeToParcel(mockParcel, 0); 193 | 194 | // Default values are null, now test with empty values 195 | testItem.setTitle(""); 196 | testItem.setTag(""); 197 | testItem.setSource(""); 198 | testItem.setPreviewSource(""); 199 | 200 | testItem.writeToParcel(mockParcel, 0); 201 | } 202 | 203 | /** 204 | * Verifies that the Parcelable interface to describe contents returns 0. 205 | */ 206 | @Test 207 | public void testDescribeContents() { 208 | final MediaItem testItem = new MediaItem(); 209 | final int expectedContents = 0; 210 | 211 | Assert.assertEquals(expectedContents, testItem.describeContents()); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/source/MediaSourceCaptureImage.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker.source; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.hardware.Camera; 8 | import android.net.Uri; 9 | import android.os.AsyncTask; 10 | import android.os.Parcel; 11 | import android.provider.MediaStore; 12 | import android.view.LayoutInflater; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.view.ViewTreeObserver; 16 | import android.widget.FrameLayout; 17 | import android.widget.ImageView; 18 | 19 | import com.android.volley.toolbox.ImageLoader; 20 | 21 | import org.wordpress.mediapicker.MediaItem; 22 | import org.wordpress.mediapicker.MediaUtils; 23 | import org.wordpress.mediapicker.R; 24 | 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | 28 | public class MediaSourceCaptureImage implements MediaSource, Camera.PictureCallback { 29 | private final static String HAS_PICTURE_KEY = "has-taken-picture"; 30 | 31 | private final MediaItem mMediaItem; 32 | 33 | // Deprecated in API 21 but not included in the support library, update when (min API >= 21) 34 | private CameraPreview mCameraPreview; 35 | private CameraPreview mFullscreenPreview; 36 | 37 | private Context mContext; 38 | private boolean mIsCaptureReady; 39 | private boolean mHasTakenPicture; 40 | 41 | // Captured images 42 | private List mCapturedImages = new ArrayList<>(); 43 | 44 | private OnMediaChange mMediaListener; 45 | 46 | public MediaSourceCaptureImage() { 47 | mMediaItem = new MediaItem(); 48 | mIsCaptureReady = false; 49 | mHasTakenPicture = false; 50 | mCameraPreview = null; 51 | } 52 | 53 | @Override 54 | public void gather(Context context) { 55 | 56 | } 57 | 58 | @Override 59 | public void cleanup() { 60 | 61 | } 62 | 63 | @Override 64 | public void setListener(OnMediaChange listener) { 65 | mMediaListener = listener; 66 | } 67 | 68 | @Override 69 | public int getCount() { 70 | return 1 + mCapturedImages.size(); 71 | } 72 | 73 | @Override 74 | public MediaItem getMedia(int position) { 75 | if (position > 0) { 76 | return mCapturedImages.get(position - 1); 77 | } 78 | 79 | return mMediaItem; 80 | } 81 | 82 | @Override 83 | public View getView(final int position, View convertView, ViewGroup parent, LayoutInflater inflater, final ImageLoader.ImageCache cache) { 84 | if (convertView == null) { 85 | convertView = inflater.inflate(R.layout.media_item_capture, parent, false); 86 | } 87 | 88 | mContext = inflater.getContext(); 89 | 90 | if (position > 0) { 91 | final ImageView overlayView = (ImageView) convertView.findViewById(R.id.capture_view_overlay); 92 | if (overlayView != null) { 93 | int width = overlayView.getWidth(); 94 | int height = overlayView.getHeight(); 95 | 96 | if (width <= 0 || height <= 0) { 97 | overlayView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 98 | @Override 99 | public boolean onPreDraw() { 100 | int width = overlayView.getWidth(); 101 | int height = overlayView.getHeight(); 102 | setImage(mCapturedImages.get(position - 1).getSource(), cache, overlayView, mCapturedImages.get(position - 1), width, height); 103 | overlayView.getViewTreeObserver().removeOnPreDrawListener(this); 104 | return true; 105 | } 106 | }); 107 | } else { 108 | setImage(mCapturedImages.get(position - 1).getSource(), cache, overlayView, mCapturedImages.get(position - 1), width, height); 109 | } 110 | } 111 | } else { 112 | if (mCameraPreview == null) { 113 | mCameraPreview = (CameraPreview) convertView.findViewById(R.id.capture_view_preview); 114 | mCameraPreview.setHighRes(false); 115 | mCameraPreview.setCamera(getBackCameraId()); 116 | } 117 | 118 | ImageView overlayView = (ImageView) convertView.findViewById(R.id.capture_view_overlay); 119 | 120 | if (overlayView != null) { 121 | overlayView.setImageResource(getOverlayResource()); 122 | } 123 | } 124 | 125 | return convertView; 126 | } 127 | 128 | private void setImage(Uri imageSource, ImageLoader.ImageCache cache, ImageView imageView, MediaItem mediaItem, int width, int height) { 129 | if (imageSource != null) { 130 | Bitmap imageBitmap = null; 131 | if (cache != null) { 132 | imageBitmap = cache.getBitmap(imageSource.toString()); 133 | } 134 | 135 | if (imageBitmap == null) { 136 | imageView.setImageResource(R.drawable.media_item_placeholder); 137 | MediaUtils.BackgroundFetchThumbnail bgDownload = 138 | new MediaUtils.BackgroundFetchThumbnail(imageView, 139 | cache, 140 | MediaUtils.BackgroundFetchThumbnail.TYPE_IMAGE, 141 | width, 142 | height, 143 | mediaItem.getRotation()); 144 | imageView.setTag(bgDownload); 145 | bgDownload.executeWithLimit(imageSource); 146 | } else { 147 | MediaUtils.fadeInImage(imageView, imageBitmap); 148 | } 149 | } else { 150 | imageView.setTag(null); 151 | imageView.setImageResource(R.drawable.media_item_placeholder); 152 | } 153 | } 154 | 155 | @Override 156 | public boolean onMediaItemSelected(final MediaItem mediaItem, boolean selected) { 157 | if (!mIsCaptureReady) { 158 | final int cameraId = mCameraPreview.getCameraId(); 159 | mCameraPreview.releaseCamera(); 160 | 161 | // TODO: necessary? 162 | ((Activity) mContext).runOnUiThread(new Runnable() { 163 | @Override 164 | public void run() { 165 | mFullscreenPreview = new CameraPreview(mContext); 166 | mFullscreenPreview.setHighRes(true); 167 | mFullscreenPreview.setCanTakePicture(true); 168 | mFullscreenPreview.setCameraCallbacks(null, null, MediaSourceCaptureImage.this); 169 | mFullscreenPreview.setCamera(cameraId); 170 | } 171 | }); 172 | 173 | FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 174 | ((Activity) mContext).addContentView(mFullscreenPreview, params); 175 | 176 | mCameraPreview.setVisibility(View.INVISIBLE); 177 | mIsCaptureReady = true; 178 | 179 | return true; 180 | } 181 | 182 | return false; 183 | } 184 | 185 | protected int getOverlayResource() { 186 | return R.drawable.camera; 187 | } 188 | 189 | @Override 190 | public void onPictureTaken(byte[] data, Camera camera) { 191 | if (mContext != null) { 192 | BitmapFactory.Options options = new BitmapFactory.Options(); 193 | options.inSampleSize = 5; 194 | 195 | // sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(imageAdded))); 196 | Bitmap imageBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options); 197 | String url = MediaStore.Images.Media.insertImage(mContext.getContentResolver(), imageBitmap, "camera-image.jpg", "description"); 198 | 199 | MediaItem capturedItem = new MediaItem(); 200 | capturedItem.setSource(url); 201 | mCapturedImages.add(capturedItem); 202 | if (mMediaListener != null) { 203 | List newItem = new ArrayList<>(); 204 | newItem.add(capturedItem); 205 | mMediaListener.onMediaAdded(this, newItem); 206 | } 207 | 208 | mHasTakenPicture = true; 209 | mIsCaptureReady = false; 210 | 211 | mFullscreenPreview.releaseCamera(); 212 | mFullscreenPreview.setVisibility(View.INVISIBLE); 213 | 214 | ((ViewGroup) mFullscreenPreview.getParent()).removeView(mFullscreenPreview); 215 | 216 | mCameraPreview.setVisibility(View.VISIBLE); 217 | ((Activity) mContext).runOnUiThread(new Runnable() { 218 | @Override 219 | public void run() { 220 | mCameraPreview.setCamera(getBackCameraId()); 221 | mCameraPreview.prepareCamera(); 222 | mCameraPreview.startCameraPreview(mCameraPreview.getHolder()); 223 | } 224 | }); 225 | } 226 | } 227 | 228 | /** 229 | * Helper method; determines ID of back-facing camera 230 | * 231 | * @return 232 | * back-facing camera ID or -1 if none could be found 233 | */ 234 | private int getBackCameraId() { 235 | Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); 236 | int cameraCount = Camera.getNumberOfCameras(); 237 | 238 | for (int cameraId = 0; cameraId < cameraCount; ++cameraId) { 239 | Camera.getCameraInfo(cameraId, cameraInfo); 240 | 241 | if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { 242 | return cameraId; 243 | } 244 | } 245 | 246 | return -1; 247 | } 248 | 249 | /* 250 | Parcelable interface 251 | */ 252 | public static final Creator CREATOR = 253 | new Creator() { 254 | public MediaSourceCaptureImage createFromParcel(Parcel in) { 255 | return new MediaSourceCaptureImage(); 256 | } 257 | 258 | public MediaSourceCaptureImage[] newArray(int size) { 259 | return new MediaSourceCaptureImage[size]; 260 | } 261 | }; 262 | 263 | @Override 264 | public int describeContents() { 265 | return 0; 266 | } 267 | 268 | @Override 269 | public void writeToParcel(Parcel dest, int flags) { 270 | if (mHasTakenPicture) { 271 | dest.writeString(HAS_PICTURE_KEY); 272 | } 273 | 274 | if (mCameraPreview != null) { 275 | mCameraPreview.releaseCamera(); 276 | } 277 | 278 | // TODO: save captured images 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /sample/src/main/java/org/wordpress/mediapickersample/SlidingTabLayout.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.wordpress.mediapickersample; 18 | 19 | import android.content.Context; 20 | import android.graphics.Typeface; 21 | import android.os.Build; 22 | import android.support.v4.view.PagerAdapter; 23 | import android.support.v4.view.ViewPager; 24 | import android.util.AttributeSet; 25 | import android.util.TypedValue; 26 | import android.view.Gravity; 27 | import android.view.LayoutInflater; 28 | import android.view.View; 29 | import android.widget.HorizontalScrollView; 30 | import android.widget.TextView; 31 | 32 | /** 33 | * To be used with ViewPager to provide a tab indicator component which give constant feedback as to 34 | * the user's scroll progress. 35 | *

36 | * To use the component, simply add it to your view hierarchy. Then in your 37 | * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call 38 | * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for. 39 | *

40 | * The colors can be customized in two ways. The first and simplest is to provide an array of colors 41 | * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The 42 | * alternative is via the {@link TabColorizer} interface which provides you complete control over 43 | * which color is used for any individual position. 44 | *

45 | * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, 46 | * providing the layout ID of your custom layout. 47 | */ 48 | public class SlidingTabLayout extends HorizontalScrollView { 49 | 50 | /** 51 | * Allows complete control over the colors drawn in the tab layout. Set with 52 | * {@link #setCustomTabColorizer(TabColorizer)}. 53 | */ 54 | public interface TabColorizer { 55 | 56 | /** 57 | * @return return the color of the indicator used when {@code position} is selected. 58 | */ 59 | int getIndicatorColor(int position); 60 | 61 | /** 62 | * @return return the color of the divider drawn to the right of {@code position}. 63 | */ 64 | int getDividerColor(int position); 65 | 66 | } 67 | 68 | private static final int TITLE_OFFSET_DIPS = 24; 69 | private static final int TAB_VIEW_PADDING_DIPS = 16; 70 | private static final int TAB_VIEW_TEXT_SIZE_SP = 12; 71 | 72 | private int mTitleOffset; 73 | 74 | private int mTabViewLayoutId; 75 | private int mTabViewTextViewId; 76 | 77 | private ViewPager mViewPager; 78 | private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; 79 | 80 | private final SlidingTabStrip mTabStrip; 81 | 82 | public SlidingTabLayout(Context context) { 83 | this(context, null); 84 | } 85 | 86 | public SlidingTabLayout(Context context, AttributeSet attrs) { 87 | this(context, attrs, 0); 88 | } 89 | 90 | public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { 91 | super(context, attrs, defStyle); 92 | 93 | // Disable the Scroll Bar 94 | setHorizontalScrollBarEnabled(false); 95 | // Make sure that the Tab Strips fills this View 96 | setFillViewport(true); 97 | 98 | mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); 99 | 100 | mTabStrip = new SlidingTabStrip(context); 101 | addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 102 | } 103 | 104 | /** 105 | * Set the custom {@link TabColorizer} to be used. 106 | * 107 | * If you only require simple custmisation then you can use 108 | * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve 109 | * similar effects. 110 | */ 111 | public void setCustomTabColorizer(TabColorizer tabColorizer) { 112 | mTabStrip.setCustomTabColorizer(tabColorizer); 113 | } 114 | 115 | /** 116 | * Sets the colors to be used for indicating the selected tab. These colors are treated as a 117 | * circular array. Providing one color will mean that all tabs are indicated with the same color. 118 | */ 119 | public void setSelectedIndicatorColors(int... colors) { 120 | mTabStrip.setSelectedIndicatorColors(colors); 121 | } 122 | 123 | /** 124 | * Sets the colors to be used for tab dividers. These colors are treated as a circular array. 125 | * Providing one color will mean that all tabs are indicated with the same color. 126 | */ 127 | public void setDividerColors(int... colors) { 128 | mTabStrip.setDividerColors(colors); 129 | } 130 | 131 | /** 132 | * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are 133 | * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so 134 | * that the layout can update it's scroll position correctly. 135 | * 136 | * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) 137 | */ 138 | public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { 139 | mViewPagerPageChangeListener = listener; 140 | } 141 | 142 | /** 143 | * Set the custom layout to be inflated for the tab views. 144 | * 145 | * @param layoutResId Layout id to be inflated 146 | * @param textViewId id of the {@link TextView} in the inflated view 147 | */ 148 | public void setCustomTabView(int layoutResId, int textViewId) { 149 | mTabViewLayoutId = layoutResId; 150 | mTabViewTextViewId = textViewId; 151 | } 152 | 153 | /** 154 | * Sets the associated view pager. Note that the assumption here is that the pager content 155 | * (number of tabs and tab titles) does not change after this call has been made. 156 | */ 157 | public void setViewPager(ViewPager viewPager) { 158 | mTabStrip.removeAllViews(); 159 | 160 | mViewPager = viewPager; 161 | if (viewPager != null) { 162 | viewPager.setOnPageChangeListener(new InternalViewPagerListener()); 163 | populateTabStrip(); 164 | } 165 | } 166 | 167 | /** 168 | * Create a default view to be used for tabs. This is called if a custom tab view is not set via 169 | * {@link #setCustomTabView(int, int)}. 170 | */ 171 | protected TextView createDefaultTabView(Context context) { 172 | TextView textView = new TextView(context); 173 | textView.setGravity(Gravity.CENTER); 174 | textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); 175 | textView.setTypeface(Typeface.DEFAULT_BOLD); 176 | 177 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 178 | // If we're running on Honeycomb or newer, then we can use the Theme's 179 | // selectableItemBackground to ensure that the View has a pressed state 180 | TypedValue outValue = new TypedValue(); 181 | getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, 182 | outValue, true); 183 | textView.setBackgroundResource(outValue.resourceId); 184 | } 185 | 186 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 187 | // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style 188 | textView.setAllCaps(true); 189 | } 190 | 191 | int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); 192 | textView.setPadding(padding, padding, padding, padding); 193 | 194 | return textView; 195 | } 196 | 197 | private void populateTabStrip() { 198 | final PagerAdapter adapter = mViewPager.getAdapter(); 199 | final View.OnClickListener tabClickListener = new TabClickListener(); 200 | 201 | for (int i = 0; i < adapter.getCount(); i++) { 202 | View tabView = null; 203 | TextView tabTitleView = null; 204 | 205 | if (mTabViewLayoutId != 0) { 206 | // If there is a custom tab view layout id set, try and inflate it 207 | tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, 208 | false); 209 | tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId); 210 | } 211 | 212 | if (tabView == null) { 213 | tabView = createDefaultTabView(getContext()); 214 | } 215 | 216 | if (tabTitleView == null && TextView.class.isInstance(tabView)) { 217 | tabTitleView = (TextView) tabView; 218 | } 219 | 220 | tabTitleView.setText(adapter.getPageTitle(i)); 221 | tabView.setOnClickListener(tabClickListener); 222 | 223 | mTabStrip.addView(tabView); 224 | } 225 | } 226 | 227 | @Override 228 | protected void onAttachedToWindow() { 229 | super.onAttachedToWindow(); 230 | 231 | if (mViewPager != null) { 232 | scrollToTab(mViewPager.getCurrentItem(), 0); 233 | } 234 | } 235 | 236 | private void scrollToTab(int tabIndex, int positionOffset) { 237 | final int tabStripChildCount = mTabStrip.getChildCount(); 238 | if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { 239 | return; 240 | } 241 | 242 | View selectedChild = mTabStrip.getChildAt(tabIndex); 243 | if (selectedChild != null) { 244 | int targetScrollX = selectedChild.getLeft() + positionOffset; 245 | 246 | if (tabIndex > 0 || positionOffset > 0) { 247 | // If we're not at the first child and are mid-scroll, make sure we obey the offset 248 | targetScrollX -= mTitleOffset; 249 | } 250 | 251 | scrollTo(targetScrollX, 0); 252 | } 253 | } 254 | 255 | private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { 256 | private int mScrollState; 257 | 258 | @Override 259 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 260 | int tabStripChildCount = mTabStrip.getChildCount(); 261 | if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { 262 | return; 263 | } 264 | 265 | mTabStrip.onViewPagerPageChanged(position, positionOffset); 266 | 267 | View selectedTitle = mTabStrip.getChildAt(position); 268 | int extraOffset = (selectedTitle != null) 269 | ? (int) (positionOffset * selectedTitle.getWidth()) 270 | : 0; 271 | scrollToTab(position, extraOffset); 272 | 273 | if (mViewPagerPageChangeListener != null) { 274 | mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, 275 | positionOffsetPixels); 276 | } 277 | } 278 | 279 | @Override 280 | public void onPageScrollStateChanged(int state) { 281 | mScrollState = state; 282 | 283 | if (mViewPagerPageChangeListener != null) { 284 | mViewPagerPageChangeListener.onPageScrollStateChanged(state); 285 | } 286 | } 287 | 288 | @Override 289 | public void onPageSelected(int position) { 290 | if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { 291 | mTabStrip.onViewPagerPageChanged(position, 0f); 292 | scrollToTab(position, 0); 293 | } 294 | 295 | if (mViewPagerPageChangeListener != null) { 296 | mViewPagerPageChangeListener.onPageSelected(position); 297 | } 298 | } 299 | 300 | } 301 | 302 | private class TabClickListener implements View.OnClickListener { 303 | @Override 304 | public void onClick(View v) { 305 | for (int i = 0; i < mTabStrip.getChildCount(); i++) { 306 | if (v == mTabStrip.getChildAt(i)) { 307 | mViewPager.setCurrentItem(i); 308 | return; 309 | } 310 | } 311 | } 312 | } 313 | 314 | } 315 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/MediaUtils.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker; 2 | 3 | import android.content.ContentResolver; 4 | import android.database.Cursor; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.Matrix; 8 | import android.media.ThumbnailUtils; 9 | import android.net.Uri; 10 | import android.os.AsyncTask; 11 | import android.provider.MediaStore; 12 | import android.view.animation.AlphaAnimation; 13 | import android.view.animation.Animation; 14 | import android.widget.ImageView; 15 | 16 | import com.android.volley.toolbox.ImageLoader; 17 | 18 | import java.lang.ref.WeakReference; 19 | import java.util.ArrayList; 20 | import java.util.HashMap; 21 | import java.util.LinkedList; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Queue; 25 | 26 | public class MediaUtils { 27 | private static final long FADE_TIME_MS = 250; 28 | 29 | public static void fadeInImage(ImageView imageView, Bitmap image) { 30 | fadeInImage(imageView, image, FADE_TIME_MS); 31 | } 32 | 33 | public static void fadeInImage(ImageView imageView, Bitmap image, long duration) { 34 | if (imageView != null) { 35 | imageView.setImageBitmap(image); 36 | Animation alpha = new AlphaAnimation(0.25f, 1.0f); 37 | alpha.setDuration(duration); 38 | imageView.startAnimation(alpha); 39 | // Use the implementation below if you can figure out how to make it work on all devices 40 | // My Galaxy S3 (4.1.2) would not animate 41 | // imageView.setImageBitmap(image); 42 | // ObjectAnimator.ofFloat(imageView, View.ALPHA, 0.25f, 1.0f).setDuration(duration).start(); 43 | } 44 | } 45 | 46 | public static Cursor getMediaStoreThumbnails(ContentResolver contentResolver, String[] columns) { 47 | if (contentResolver == null) return null; 48 | Uri thumbnailUri = MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI; 49 | return MediaStore.Images.Thumbnails.query(contentResolver, thumbnailUri, columns); 50 | } 51 | 52 | public static Cursor getDeviceMediaStoreVideos(ContentResolver contentResolver, String[] columns) { 53 | if (contentResolver == null) return null; 54 | Uri videoUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 55 | return MediaStore.Video.query(contentResolver, videoUri, columns); 56 | } 57 | 58 | public static Map getMediaStoreThumbnailData(Cursor thumbnailCursor, 59 | String dataColumnName, 60 | String idColumnName) { 61 | final Map data = new HashMap<>(); 62 | 63 | if (thumbnailCursor != null) { 64 | if (thumbnailCursor.moveToFirst()) { 65 | do { 66 | int dataColumnIndex = thumbnailCursor.getColumnIndex(dataColumnName); 67 | int imageIdColumnIndex = thumbnailCursor.getColumnIndex(idColumnName); 68 | 69 | if (dataColumnIndex != -1 && imageIdColumnIndex != -1) { 70 | data.put(thumbnailCursor.getString(imageIdColumnIndex), 71 | thumbnailCursor.getString(dataColumnIndex)); 72 | } 73 | } while (thumbnailCursor.moveToNext()); 74 | } 75 | 76 | thumbnailCursor.close(); 77 | } 78 | 79 | return data; 80 | } 81 | 82 | public static List createMediaItems(Map thumbnailData, Cursor mediaCursor, int type) { 83 | final List mediaItems = new ArrayList<>(); 84 | final List ids = new ArrayList<>(); 85 | 86 | if (mediaCursor != null) { 87 | if (mediaCursor.moveToFirst()) { 88 | do { 89 | MediaItem newContent = type == BackgroundFetchThumbnail.TYPE_IMAGE ? 90 | getMediaItemFromImageCursor(mediaCursor, thumbnailData) : 91 | getMediaItemFromVideoCursor(mediaCursor, thumbnailData); 92 | 93 | if (newContent != null && !ids.contains(newContent.getTag())) { 94 | mediaItems.add(newContent); 95 | ids.add(newContent.getTag()); 96 | } 97 | } while (mediaCursor.moveToNext()); 98 | } 99 | 100 | mediaCursor.close(); 101 | } 102 | 103 | return mediaItems; 104 | } 105 | 106 | public static void fadeMediaItemImageIntoView(Uri imageSource, ImageLoader.ImageCache cache, 107 | ImageView imageView, MediaItem mediaItem, 108 | int width, int height, int type) { 109 | if (imageSource != null && !imageSource.toString().isEmpty()) { 110 | Bitmap imageBitmap = null; 111 | if (cache != null) { 112 | imageBitmap = cache.getBitmap(imageSource.toString()); 113 | } 114 | 115 | if (imageBitmap == null) { 116 | imageView.setImageResource(R.drawable.media_item_placeholder); 117 | BackgroundFetchThumbnail bgDownload = 118 | new MediaUtils.BackgroundFetchThumbnail(imageView, 119 | cache, 120 | type, 121 | width, 122 | height, 123 | mediaItem.getRotation()); 124 | imageView.setTag(bgDownload); 125 | bgDownload.executeWithLimit(imageSource); 126 | } else { 127 | fadeInImage(imageView, imageBitmap); 128 | } 129 | } else { 130 | imageView.setTag(null); 131 | imageView.setImageResource(R.drawable.ic_now_wallpaper_white); 132 | } 133 | } 134 | 135 | /** 136 | * Implementation of AsyncTask that limits the number of executions. Child classes must call 137 | * super.* methods for all of onPreExecute/doInBackground/onPostExecute/onCancelled or none. 138 | * 139 | * Subclasses are passed a generic Object in startExecution and it is expected to be cast to 140 | * the appropriate type when calling execute/executeOnExecutor. 141 | */ 142 | public static abstract class LimitedBackgroundOperation 143 | extends AsyncTask { 144 | private static final int MAX_FETCHES = 16; 145 | private static final Queue sFetchQueue = new LinkedList<>(); 146 | 147 | private static int sNumFetching = 0; 148 | 149 | private Params mParams; 150 | 151 | @Override 152 | protected final void onPreExecute() { 153 | performPreExecute(); 154 | ++sNumFetching; 155 | } 156 | 157 | @Override 158 | protected final Result doInBackground(Params... params) { 159 | return performBackgroundOperation(params); 160 | } 161 | 162 | @Override 163 | protected final void onPostExecute(Result result) { 164 | performPostExecute(result); 165 | continueExclusiveExecution(); 166 | } 167 | 168 | @Override 169 | protected final void onCancelled(Result result) { 170 | performCancelled(result); 171 | continueExclusiveExecution(); 172 | } 173 | 174 | protected void performPreExecute() { 175 | } 176 | 177 | // Required 178 | protected abstract Result performBackgroundOperation(Params... params); 179 | 180 | protected void performPostExecute(Result result) { 181 | } 182 | 183 | protected void performCancelled(Result result) { 184 | } 185 | 186 | // Should invoke execute or executeOnExecutor 187 | public abstract void startExecution(Object params); 188 | 189 | public void executeWithLimit(Params params) { 190 | mParams = params; 191 | startExclusiveExecution(); 192 | } 193 | 194 | private void startExclusiveExecution() { 195 | if (sNumFetching < MAX_FETCHES) { 196 | startExecution(mParams); 197 | } else { 198 | sFetchQueue.add(this); 199 | } 200 | } 201 | 202 | private void continueExclusiveExecution() { 203 | if (--sNumFetching < MAX_FETCHES && sFetchQueue.size() > 0) { 204 | LimitedBackgroundOperation next = sFetchQueue.remove(); 205 | if (next != null) { 206 | next.startExecution(next.mParams); 207 | } 208 | } 209 | } 210 | } 211 | 212 | public static class BackgroundFetchThumbnail extends LimitedBackgroundOperation { 213 | public static final int TYPE_IMAGE = 0; 214 | public static final int TYPE_VIDEO = 1; 215 | 216 | private WeakReference mReference; 217 | private ImageLoader.ImageCache mCache; 218 | private int mType; 219 | private int mWidth; 220 | private int mHeight; 221 | private int mRotation; 222 | 223 | public BackgroundFetchThumbnail(ImageView resultStore, ImageLoader.ImageCache cache, int type, int width, int height, int rotation) { 224 | mReference = new WeakReference<>(resultStore); 225 | mCache = cache; 226 | mType = type; 227 | mWidth = width; 228 | mHeight = height; 229 | mRotation = rotation; 230 | } 231 | 232 | @Override 233 | protected Bitmap performBackgroundOperation(Uri... params) { 234 | String uri = params[0].toString(); 235 | Bitmap bitmap = null; 236 | 237 | if (mType == TYPE_IMAGE) { 238 | BitmapFactory.Options options = new BitmapFactory.Options(); 239 | options.inJustDecodeBounds = true; 240 | BitmapFactory.decodeFile(uri, options); 241 | options.inJustDecodeBounds = false; 242 | options.inSampleSize = calculateInSampleSize(options); 243 | bitmap = BitmapFactory.decodeFile(uri, options); 244 | 245 | if (bitmap != null) { 246 | Matrix rotation = new Matrix(); 247 | rotation.setRotate(mRotation, bitmap.getWidth() / 2.0f, bitmap.getHeight() / 2.0f); 248 | bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), rotation, false); 249 | } 250 | } else if (mType == TYPE_VIDEO) { 251 | // MICRO_KIND = 96 x 96 252 | // MINI_KIND = 512 x 384 253 | bitmap = ThumbnailUtils.createVideoThumbnail(uri, MediaStore.Video.Thumbnails.MINI_KIND); 254 | } 255 | 256 | if (mCache != null && bitmap != null) { 257 | mCache.putBitmap(uri, bitmap); 258 | } 259 | 260 | return bitmap; 261 | } 262 | 263 | @Override 264 | protected void performPostExecute(Bitmap result) { 265 | ImageView imageView = mReference.get(); 266 | 267 | if (imageView != null) { 268 | if (imageView.getTag() == this) { 269 | imageView.setTag(null); 270 | if (result == null) { 271 | imageView.setImageResource(R.drawable.ic_now_wallpaper_white); 272 | } else { 273 | fadeInImage(imageView, result); 274 | } 275 | } 276 | } 277 | } 278 | 279 | @Override 280 | public void startExecution(Object params) { 281 | if (!(params instanceof Uri)) { 282 | throw new IllegalArgumentException("Params must of type Uri"); 283 | } 284 | 285 | executeOnExecutor(THREAD_POOL_EXECUTOR, (Uri) params); 286 | } 287 | 288 | // http://developer.android.com/training/displaying-bitmaps/load-bitmap.html 289 | private int calculateInSampleSize(BitmapFactory.Options options) { 290 | // Raw height and width of image 291 | final int height = options.outHeight; 292 | final int width = options.outWidth; 293 | int inSampleSize = 1; 294 | 295 | if (height > mHeight || width > mWidth) { 296 | 297 | final int halfHeight = height / 2; 298 | final int halfWidth = width / 2; 299 | 300 | // Calculate the largest inSampleSize value that is a power of 2 and keeps both 301 | // height and width larger than the requested height and width. 302 | while ((halfHeight / inSampleSize) > mHeight 303 | && (halfWidth / inSampleSize) > mWidth) { 304 | inSampleSize *= 2; 305 | } 306 | } 307 | 308 | return inSampleSize; 309 | } 310 | } 311 | 312 | private static MediaItem getMediaItemFromVideoCursor(Cursor videoCursor, Map thumbnailData) { 313 | MediaItem newContent = null; 314 | 315 | int videoIdColumnIndex = videoCursor.getColumnIndex(MediaStore.Video.Media._ID); 316 | int videoDataColumnIndex = videoCursor.getColumnIndex(MediaStore.Video.Media.DATA); 317 | 318 | if (videoIdColumnIndex != -1) { 319 | newContent = new MediaItem(); 320 | newContent.setTag(videoCursor.getString(videoIdColumnIndex)); 321 | newContent.setTitle(""); 322 | 323 | if (videoDataColumnIndex != -1) { 324 | String videoSource = videoCursor.getString(videoDataColumnIndex); 325 | if (videoSource == null) { 326 | return null; 327 | } 328 | newContent.setSource(Uri.parse(videoSource)); 329 | } 330 | if (thumbnailData.containsKey(newContent.getTag())) { 331 | String thumbnailSource = thumbnailData.get(newContent.getTag()); 332 | if (thumbnailSource == null) { 333 | return null; 334 | } 335 | newContent.setPreviewSource(Uri.parse(thumbnailSource)); 336 | } 337 | } 338 | 339 | return newContent; 340 | } 341 | 342 | private static MediaItem getMediaItemFromImageCursor(Cursor imageCursor, Map thumbnailData) { 343 | MediaItem newContent = null; 344 | 345 | int imageIdColumnIndex = imageCursor.getColumnIndex(MediaStore.Images.Media._ID); 346 | int imageDataColumnIndex = imageCursor.getColumnIndex(MediaStore.Images.Media.DATA); 347 | int imageOrientationColumnIndex = imageCursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION); 348 | 349 | if (imageIdColumnIndex != -1) { 350 | newContent = new MediaItem(); 351 | newContent.setTag(imageCursor.getString(imageIdColumnIndex)); 352 | newContent.setTitle(""); 353 | 354 | if (imageDataColumnIndex != -1) { 355 | String imageSource = imageCursor.getString(imageDataColumnIndex); 356 | if (imageSource == null) { 357 | return null; 358 | } 359 | newContent.setSource(Uri.parse(imageSource)); 360 | } 361 | if (thumbnailData.containsKey(newContent.getTag())) { 362 | String thumbnailSource = thumbnailData.get(newContent.getTag()); 363 | if (thumbnailSource == null) { 364 | return null; 365 | } 366 | newContent.setPreviewSource(Uri.parse(thumbnailSource)); 367 | } 368 | if (imageOrientationColumnIndex != -1) { 369 | newContent.setRotation(imageCursor.getInt(imageOrientationColumnIndex)); 370 | } 371 | } 372 | 373 | return newContent; 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /mediapicker/src/main/java/org/wordpress/mediapicker/MediaPickerFragment.java: -------------------------------------------------------------------------------- 1 | package org.wordpress.mediapicker; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.content.res.Resources; 6 | import android.graphics.drawable.Drawable; 7 | import android.os.Bundle; 8 | import android.view.ActionMode; 9 | import android.view.LayoutInflater; 10 | import android.view.Menu; 11 | import android.view.MenuInflater; 12 | import android.view.MenuItem; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.AbsListView; 16 | import android.widget.AdapterView; 17 | import android.widget.TextView; 18 | 19 | import com.android.volley.toolbox.ImageLoader; 20 | 21 | import org.wordpress.mediapicker.source.MediaSource; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | /** 27 | * MediaPickerFragment tracks a collection of MediaSources and responds to changes via the 28 | * {@link org.wordpress.mediapicker.source.MediaSource.OnMediaChange} interface. 29 | * 30 | * If the host Activity implements {@link org.wordpress.mediapicker.MediaPickerFragment.OnMediaSelected} 31 | * it will automatically be set as the listener, otherwise a setter is provided. This interface is 32 | * how user intent is delivered, while not having a listener won't break anything it's not very useful. 33 | * 34 | * By default the {@link org.wordpress.mediapicker.MediaSourceAdapter} is shown within 35 | * a {@link android.widget.GridView}, but a subclass of {@link android.widget.AbsListView} may be provided 36 | * with id=media_adapter_view. A subclass of {@link android.widget.TextView} may also be provided 37 | * for the empty view; id=media_empty_view. You can even provide a subclass of 38 | * {@link org.wordpress.mediapicker.MediaSourceAdapter}. 39 | * 40 | * Menu items may be provided for Action Mode and their selection will be alerted with onMenuItemSelected. 41 | * A selection confirmation button is automatically added and will call onMediaSelectionConfirmed when selected. 42 | */ 43 | 44 | public class MediaPickerFragment extends Fragment 45 | implements AdapterView.OnItemClickListener, 46 | AbsListView.MultiChoiceModeListener, 47 | MediaSource.OnMediaChange { 48 | public static final String KEY_SELECTED_CONTENT = "key-selected-content"; 49 | public static final String KEY_MEDIA_SOURCES = "key-media-sources"; 50 | public static final String KEY_CUSTOM_LAYOUT = "key-custom-view"; 51 | public static final String KEY_ACTION_MODE_MENU = "key-action-mode-menu"; 52 | public static final String KEY_LOADING_TEXT = "key-loading-text"; 53 | public static final String KEY_EMPTY_TEXT = "key-empty-text"; 54 | public static final String KEY_ERROR_TEXT = "key-error-text"; 55 | 56 | // Default layout to be used if a custom layout is not provided 57 | private static final int DEFAULT_VIEW = R.layout.media_picker_fragment; 58 | 59 | /** 60 | * Interface to respond to user intent and provide a caching mechanism for the fragment. 61 | */ 62 | public interface OnMediaSelected { 63 | // Called when the first item is selected 64 | public void onMediaSelectionStarted(); 65 | // Called when a new item is selected 66 | public void onMediaSelected(MediaItem mediaContent, boolean selected); 67 | // Called when the user confirms content selection 68 | public void onMediaSelectionConfirmed(ArrayList mediaContent); 69 | // Called when the last selected item is deselected 70 | public void onMediaSelectionCancelled(); 71 | // Called when a menu item has been tapped 72 | public boolean onMenuItemSelected(MenuItem menuItem, ArrayList selectedContent); 73 | // Should handle null image cache 74 | public ImageLoader.ImageCache getImageCache(); 75 | } 76 | 77 | // Current media sources and selected content from the sources 78 | private final ArrayList mMediaSources; 79 | private final ArrayList mSelectedContent; 80 | 81 | // Callbacks for media selection events, use to track user intent 82 | private OnMediaSelected mListener; 83 | 84 | // Required state tracking to prevent OnMediaSelectionCancelled from being called erroneously 85 | private boolean mConfirmed; 86 | 87 | // Views utilized by this fragment 88 | private TextView mEmptyView; 89 | private AbsListView mAdapterView; 90 | 91 | // Adapter for showing MediaSource content in the AdapterView 92 | private MediaSourceAdapter mAdapter; 93 | 94 | // Customizable view resources, some default behavior is defined as described in the docs 95 | private int mCustomLayout; 96 | private int mActionModeMenu; 97 | 98 | // Customizable status text messages, default values are provided 99 | private String mLoadingText; 100 | private String mEmptyText; 101 | private String mErrorText; 102 | 103 | public MediaPickerFragment() { 104 | super(); 105 | 106 | mCustomLayout = -1; 107 | mActionModeMenu = -1; 108 | mMediaSources = new ArrayList<>(); 109 | mSelectedContent = new ArrayList<>(); 110 | } 111 | 112 | @Override 113 | public void onAttach(Activity activity) { 114 | super.onAttach(activity); 115 | 116 | // Per the documentation, the host Activity is the default listener 117 | if (mListener == null && activity instanceof OnMediaSelected) { 118 | mListener = (OnMediaSelected) activity; 119 | } 120 | } 121 | 122 | @Override 123 | public void onCreate(Bundle savedInstanceState) { 124 | super.onCreate(savedInstanceState); 125 | 126 | // If this is not being restored from a previous state use arguments to set state 127 | if (savedInstanceState == null) { 128 | savedInstanceState = getArguments(); 129 | } 130 | 131 | restoreFromBundle(savedInstanceState); 132 | 133 | setDefaultTextValues(); 134 | } 135 | 136 | @Override 137 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 138 | super.onCreateView(inflater, container, savedInstanceState); 139 | 140 | restoreFromBundle(savedInstanceState); 141 | 142 | int viewToInflate = mCustomLayout < 0 ? DEFAULT_VIEW : mCustomLayout; 143 | View mediaPickerView = inflater.inflate(viewToInflate, container, false); 144 | if (mediaPickerView != null) { 145 | if (mEmptyView == null) { 146 | mEmptyView = (TextView) mediaPickerView.findViewById(R.id.media_empty_view); 147 | if (mMediaSources.size() == 0) { 148 | updateEmptyView(getString(R.string.no_media_sources)); 149 | } else { 150 | updateEmptyView(mLoadingText); 151 | } 152 | } else { 153 | mEmptyView = (TextView) mediaPickerView.findViewById(R.id.media_empty_view); 154 | if (mAdapter.getCount() == 0) { 155 | updateEmptyView(mEmptyText); 156 | } 157 | } 158 | 159 | mAdapterView = (AbsListView) mediaPickerView.findViewById(R.id.media_adapter_view); 160 | if (mAdapterView != null) { 161 | layoutAdapterView(); 162 | 163 | if (mAdapter == null) { 164 | generateAdapter(); 165 | } else { 166 | mAdapterView.setAdapter(mAdapter); 167 | mAdapter.notifyDataSetChanged(); 168 | } 169 | toggleEmptyVisibility(); 170 | } 171 | } 172 | 173 | return mediaPickerView; 174 | } 175 | 176 | @Override 177 | public void onDestroy() { 178 | super.onDestroy(); 179 | 180 | cleanupMediaSources(); 181 | } 182 | 183 | @Override 184 | public void onSaveInstanceState(Bundle outState) { 185 | super.onSaveInstanceState(outState); 186 | 187 | if (mSelectedContent.size() > 0) { 188 | outState.putParcelableArrayList(KEY_SELECTED_CONTENT, mSelectedContent); 189 | } 190 | 191 | if (mMediaSources.size() > 0) { 192 | outState.putParcelableArrayList(KEY_MEDIA_SOURCES, mMediaSources); 193 | } 194 | 195 | if (mCustomLayout > -1) { 196 | outState.putInt(KEY_CUSTOM_LAYOUT, mCustomLayout); 197 | } 198 | 199 | if (mActionModeMenu > -1) { 200 | outState.putInt(KEY_ACTION_MODE_MENU, mActionModeMenu); 201 | } 202 | 203 | if (mLoadingText != null) { 204 | outState.putString(KEY_LOADING_TEXT, mLoadingText); 205 | } 206 | 207 | if (mErrorText != null) { 208 | outState.putString(KEY_ERROR_TEXT, mErrorText); 209 | } 210 | 211 | if (mEmptyText != null) { 212 | outState.putString(KEY_EMPTY_TEXT, mEmptyText); 213 | } 214 | } 215 | 216 | @Override 217 | public void onItemClick(AdapterView parent, View view, int position, long id) { 218 | if (!notifyMediaSelected(position, true)) { 219 | notifyMediaSelectionConfirmed(); 220 | } 221 | } 222 | 223 | @Override 224 | public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { 225 | notifyMediaSelected(position, checked); 226 | 227 | if (checked) { 228 | if (!mSelectedContent.contains(mAdapter.getItem(position))) { 229 | mSelectedContent.add(mAdapter.getItem(position)); 230 | } 231 | } else { 232 | mSelectedContent.remove(mAdapter.getItem(position)); 233 | } 234 | 235 | mode.setTitle(getActivity().getTitle() + " (" + mSelectedContent.size() + ")"); 236 | } 237 | 238 | @Override 239 | public boolean onCreateActionMode(ActionMode mode, Menu menu) { 240 | mode.setTitle(getActivity().getTitle()); 241 | getActivity().onActionModeStarted(mode); 242 | inflateActionModeMenu(menu); 243 | 244 | return true; 245 | } 246 | 247 | @Override 248 | public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 249 | notifyMediaSelectionStarted(); 250 | mConfirmed = false; 251 | 252 | return true; 253 | } 254 | 255 | @Override 256 | public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) { 257 | if (menuItem.getItemId() == R.id.menu_media_selection_confirmed) { 258 | notifyMediaSelectionConfirmed(); 259 | mode.finish(); 260 | return true; 261 | } else if (mListener != null) { 262 | return mListener.onMenuItemSelected(menuItem, mSelectedContent); 263 | } 264 | 265 | return false; 266 | } 267 | 268 | @Override 269 | public void onDestroyActionMode(ActionMode mode) { 270 | if (!mConfirmed) { 271 | notifyMediaSelectionCancelled(); 272 | } 273 | 274 | mSelectedContent.clear(); 275 | 276 | getActivity().onActionModeFinished(mode); 277 | } 278 | 279 | @Override 280 | public void onMediaLoaded(boolean success) { 281 | if (success) { 282 | if (mAdapter != null && mAdapter.getCount() > 0) { 283 | toggleEmptyVisibility(); 284 | mAdapter.notifyDataSetChanged(); 285 | } else { 286 | updateEmptyView(mEmptyText); 287 | } 288 | } else { 289 | updateEmptyView(mErrorText); 290 | } 291 | } 292 | 293 | @Override 294 | public void onMediaAdded(MediaSource source, List addedItems) { 295 | toggleEmptyVisibility(); 296 | mAdapter.notifyDataSetChanged(); 297 | } 298 | 299 | @Override 300 | public void onMediaRemoved(MediaSource source, List removedItems) { 301 | toggleEmptyVisibility(); 302 | mAdapter.notifyDataSetChanged(); 303 | } 304 | 305 | @Override 306 | public void onMediaChanged(MediaSource source, List changedItems) { 307 | mAdapter.notifyDataSetChanged(); 308 | } 309 | 310 | /** 311 | * Sets the listener. Calling this method will overwrite the current listener. 312 | * 313 | * @param listener 314 | * the new listener, can be null 315 | */ 316 | public void setListener(OnMediaSelected listener) { 317 | mListener = listener; 318 | } 319 | 320 | /** 321 | * Sets the {@link org.wordpress.mediapicker.source.MediaSource}'s to be presented. The current 322 | * sources and selected content are cleaned up and cleared before the new list added. 323 | * 324 | * @param mediaSources 325 | * the new sources 326 | */ 327 | public void setMediaSources(ArrayList mediaSources) { 328 | mSelectedContent.clear(); 329 | 330 | cleanupMediaSources(); 331 | mMediaSources.clear(); 332 | 333 | // a null parameter results in a NullPointerException 334 | if (mediaSources != null) { 335 | mMediaSources.addAll(mediaSources); 336 | generateAdapter(); 337 | } 338 | } 339 | 340 | /** 341 | * Sets the menu resource to be inflated when in Action Mode. 342 | * 343 | * @param id 344 | * the ID of the menu resource, any value < 0 will use the default menu 345 | */ 346 | public void setActionModeMenu(int id) { 347 | mActionModeMenu = id; 348 | } 349 | 350 | /** 351 | * Sets the layout resource to be used to display media content. 352 | * 353 | * @param customLayout 354 | * the ID of the layout resource, any value < 0 will use the default layout 355 | */ 356 | public void setCustomLayout(int customLayout) { 357 | mCustomLayout = customLayout; 358 | } 359 | 360 | /** 361 | * Sets the text to be displayed while media content is loading. 362 | * 363 | * @param loadingText 364 | * the text to display while media is loading, can be null 365 | */ 366 | public void setLoadingText(String loadingText) { 367 | mLoadingText = loadingText; 368 | } 369 | 370 | /** 371 | * Same as calling {@link #setLoadingText(String)} with {@link #getString(int)} for resId. 372 | * Passing resId < 0 will set the text to null. 373 | * 374 | * @param resId 375 | * resource ID of the string to display while media is loading 376 | */ 377 | public void setLoadingText(int resId) { 378 | if (resId < 0) { 379 | setLoadingText(null); 380 | } else { 381 | setLoadingText(getString(resId)); 382 | } 383 | } 384 | 385 | /** 386 | * Sets the text to be displayed if there is no media content to show. 387 | * 388 | * @param emptyText 389 | * the text to display when there is no media, can be null 390 | */ 391 | public void setEmptyText(String emptyText) { 392 | mEmptyText = emptyText; 393 | } 394 | 395 | /** 396 | * Same as calling {@link #setEmptyText(String)} with {@link #getString(int)} for resId. 397 | * Passing resId < 0 will set the text to null. 398 | * 399 | * @param resId 400 | * resource ID of the string to display when there is no media 401 | */ 402 | public void setEmptyText(int resId) { 403 | if (resId < 0) { 404 | setEmptyText(null); 405 | } else { 406 | setEmptyText(getString(resId)); 407 | } 408 | } 409 | 410 | /** 411 | * Sets the text to be displayed if an error occurs while loading media. 412 | * 413 | * @param errorText 414 | * the text to display when an error occurs while loading, can be null 415 | */ 416 | public void setErrorText(String errorText) { 417 | mErrorText = errorText; 418 | } 419 | 420 | /** 421 | * Same as calling {@link #setErrorText(String)} with {@link #getString(int)} for resId. 422 | * Passing resId < 0 will set the text to null. 423 | * 424 | * @param resId 425 | * resource ID of the string to display when there is an error loading media 426 | */ 427 | public void setErrorText(int resId) { 428 | if (resId < 0) { 429 | setErrorText(null); 430 | } else { 431 | setErrorText(getString(resId)); 432 | } 433 | } 434 | 435 | /** 436 | * Sets the adapter. 437 | * 438 | * @param adapter 439 | * the new adapter 440 | */ 441 | public void setAdapter(MediaSourceAdapter adapter) { 442 | mAdapter = adapter; 443 | 444 | if (mAdapterView != null) { 445 | mAdapterView.setAdapter(mAdapter); 446 | } 447 | } 448 | 449 | private void updateEmptyView(String text) { 450 | if (mEmptyView != null) { 451 | mEmptyView.setText(text); 452 | } 453 | } 454 | 455 | /** 456 | * Restores state from a given {@link android.os.Bundle}. Checks for media sources, selected 457 | * content, custom view, custom action mode menu, and custom empty text. 458 | * 459 | * @param bundle 460 | * Bundle containing all the data, can be null 461 | */ 462 | private void restoreFromBundle(Bundle bundle) { 463 | if (bundle != null) { 464 | if (bundle.containsKey(KEY_MEDIA_SOURCES)) { 465 | ArrayList mediaSources = bundle.getParcelableArrayList(KEY_MEDIA_SOURCES); 466 | setMediaSources(mediaSources); 467 | 468 | if (bundle.containsKey(KEY_SELECTED_CONTENT)) { 469 | ArrayList mediaItems = bundle.getParcelableArrayList(KEY_SELECTED_CONTENT); 470 | 471 | if (mediaItems != null) { 472 | mSelectedContent.addAll(mediaItems); 473 | } 474 | } 475 | } 476 | 477 | if (bundle.containsKey(KEY_CUSTOM_LAYOUT)) { 478 | setCustomLayout(bundle.getInt(KEY_CUSTOM_LAYOUT, -1)); 479 | } 480 | 481 | if (bundle.containsKey(KEY_ACTION_MODE_MENU)) { 482 | setActionModeMenu(bundle.getInt(KEY_ACTION_MODE_MENU, -1)); 483 | } 484 | 485 | if (bundle.containsKey(KEY_LOADING_TEXT)) { 486 | mLoadingText = bundle.getString(KEY_LOADING_TEXT, mLoadingText); 487 | } 488 | 489 | if (bundle.containsKey(KEY_EMPTY_TEXT)) { 490 | mEmptyText = bundle.getString(KEY_EMPTY_TEXT, mEmptyText); 491 | } 492 | 493 | if (bundle.containsKey(KEY_ERROR_TEXT)) { 494 | mErrorText = bundle.getString(KEY_ERROR_TEXT, mErrorText); 495 | } 496 | } 497 | } 498 | 499 | /** 500 | * Sets the default empty text strings if they are not already set to something. 501 | */ 502 | private void setDefaultTextValues() { 503 | if (mLoadingText == null) setLoadingText(R.string.fetching_media); 504 | if (mEmptyText == null) setEmptyText(R.string.no_media); 505 | if (mErrorText == null) setErrorText(R.string.error_fetching_media); 506 | } 507 | 508 | /** 509 | * Calls {@link org.wordpress.mediapicker.source.MediaSource.OnMediaChange#cleanup()} on all 510 | * non-null sources. 511 | */ 512 | private void cleanupMediaSources() { 513 | for (MediaSource source : mMediaSources) { 514 | if (source != null) { 515 | source.cleanup(); 516 | } 517 | } 518 | } 519 | 520 | /** 521 | * Constructs the {@link org.wordpress.mediapicker.MediaSourceAdapter} and attaches it to the 522 | * adapter view if possible. 523 | */ 524 | private void generateAdapter() { 525 | Activity activity = getActivity(); 526 | 527 | if (activity != null) { 528 | ImageLoader.ImageCache imageCache = mListener != null ? mListener.getImageCache() : null; 529 | 530 | MediaSourceAdapter adapter = new MediaSourceAdapter(activity, mMediaSources, imageCache); 531 | adapter.gatherFromSources(this); 532 | 533 | setAdapter(adapter); 534 | } 535 | } 536 | 537 | /** 538 | * Creates the {@link org.wordpress.mediapicker.MediaSourceAdapter} and initializes the adapter 539 | * view to display it. 540 | */ 541 | private void layoutAdapterView() { 542 | // Safe to assume non-null since this is only called in onCreateView 543 | Activity activity = getActivity(); 544 | Resources resources = activity.getResources(); 545 | Drawable background = resources.getDrawable(R.drawable.media_picker_background); 546 | 547 | // Use setBackground(Drawable) when API min is >= 16 548 | mAdapterView.setBackgroundDrawable(background); 549 | mAdapterView.setClipToPadding(false); 550 | mAdapterView.setMultiChoiceModeListener(this); 551 | mAdapterView.setOnItemClickListener(this); 552 | mAdapterView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL); 553 | mAdapterView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); 554 | } 555 | 556 | /** 557 | * Inflates custom mActionModeMenu if set, otherwise inflates default media_picker_action_mode. 558 | * 559 | * @param menu 560 | * the menu to inflate to 561 | */ 562 | private void inflateActionModeMenu(Menu menu) { 563 | MenuInflater menuInflater = getActivity().getMenuInflater(); 564 | 565 | if (mActionModeMenu != -1) { 566 | menuInflater.inflate(mActionModeMenu, menu); 567 | addSelectionConfirmationButtonMenuItem(menu); 568 | } else { 569 | menuInflater.inflate(R.menu.media_picker_action_mode, menu); 570 | } 571 | } 572 | 573 | /** 574 | * Adds a menu item to confirm media selection during Action Mode. Only adds one if one is not 575 | * defined. 576 | * 577 | * @param menu 578 | * the menu to add a confirm option to 579 | */ 580 | private void addSelectionConfirmationButtonMenuItem(Menu menu) { 581 | if (menu != null && menu.findItem(R.id.menu_media_selection_confirmed) == null) { 582 | menu.add(Menu.NONE, R.id.menu_media_selection_confirmed, Menu.FIRST, R.string.confirm) 583 | .setIcon(R.drawable.action_mode_confirm_checkmark); 584 | } 585 | } 586 | 587 | /** 588 | * If the current adapter does not have any items the empty view will be shown and the adapter 589 | * view will be hidden. Otherwise the empty view will be hidden and the adapter view presented. 590 | */ 591 | private void toggleEmptyVisibility() { 592 | int empty = (mAdapter != null && mAdapter.getCount() == 0) ? View.VISIBLE : View.GONE; 593 | int adapter = (mAdapter != null && mAdapter.getCount() == 0) ? View.GONE : View.VISIBLE; 594 | 595 | if (mEmptyView != null) { 596 | mEmptyView.setVisibility(empty); 597 | } 598 | if (mAdapterView != null) { 599 | mAdapterView.setVisibility(adapter); 600 | } 601 | } 602 | 603 | /** 604 | * Notifies non-null listener that media selection has started. 605 | */ 606 | private void notifyMediaSelectionStarted() { 607 | if (mListener != null) { 608 | mListener.onMediaSelectionStarted(); 609 | } 610 | } 611 | 612 | /** 613 | * Notifies non-null listener when selection state changes on a media item. 614 | */ 615 | private boolean notifyMediaSelected(int position, boolean selected) { 616 | MediaItem mediaItem = mAdapter.getItem(position); 617 | 618 | if (mediaItem != null) { 619 | MediaSource mediaSource = mAdapter.sourceAtPosition(position); 620 | 621 | if (mediaSource == null || !mediaSource.onMediaItemSelected(mediaItem, selected)) { 622 | if (mListener != null) { 623 | mListener.onMediaSelected(mediaItem, selected); 624 | } 625 | 626 | mSelectedContent.add(mediaItem); 627 | 628 | return false; 629 | } 630 | } 631 | 632 | return true; 633 | } 634 | 635 | /** 636 | * Notifies non-null listener that media selection has been confirmed. 637 | */ 638 | private void notifyMediaSelectionConfirmed() { 639 | if (mListener != null) { 640 | mListener.onMediaSelectionConfirmed(mSelectedContent); 641 | } 642 | 643 | mConfirmed = true; 644 | } 645 | 646 | /** 647 | * Notifies non-null listener that media selection has been cancelled. 648 | */ 649 | private void notifyMediaSelectionCancelled() { 650 | if (mListener != null) { 651 | mListener.onMediaSelectionCancelled(); 652 | } 653 | } 654 | } 655 | --------------------------------------------------------------------------------