├── .editorconfig ├── .github ├── ISSUE_TEMPLATE └── PULL_REQUEST_TEMPLATE ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── loklak │ │ └── wok │ │ └── SuggestPresenterTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── org │ │ │ └── loklak │ │ │ └── wok │ │ │ ├── Constants.java │ │ │ ├── LoklakWokApplication.java │ │ │ ├── Utility.java │ │ │ ├── adapters │ │ │ ├── HarvestedTweetAdapter.java │ │ │ ├── PostTweetMediaAdapter.java │ │ │ ├── SearchCategoryAdapter.java │ │ │ ├── SearchFragmentPagerAdapter.java │ │ │ ├── SuggestAdapter.java │ │ │ └── TweetImagesAdapter.java │ │ │ ├── api │ │ │ ├── loklak │ │ │ │ ├── LoklakAPI.java │ │ │ │ └── RestClient.java │ │ │ └── twitter │ │ │ │ ├── TwitterAPI.java │ │ │ │ ├── TwitterMediaAPI.java │ │ │ │ ├── TwitterMediaRestClient.java │ │ │ │ ├── TwitterOAuthInterceptor.java │ │ │ │ ├── TwitterRestClient.java │ │ │ │ └── UrlEscapeUtils.java │ │ │ ├── inject │ │ │ ├── ApplicationComponent.java │ │ │ └── ApplicationModule.java │ │ │ ├── model │ │ │ ├── harvest │ │ │ │ ├── Push.java │ │ │ │ ├── ScrapedData.java │ │ │ │ ├── Status.java │ │ │ │ └── User.java │ │ │ ├── search │ │ │ │ ├── Search.java │ │ │ │ ├── SearchMetadata.java │ │ │ │ ├── Status.java │ │ │ │ └── User.java │ │ │ ├── suggest │ │ │ │ ├── Query.java │ │ │ │ ├── SearchMetadata.java │ │ │ │ └── SuggestData.java │ │ │ └── twitter │ │ │ │ ├── AccountVerifyCredentials.java │ │ │ │ ├── MediaEntity.java │ │ │ │ ├── MediaUpload.java │ │ │ │ ├── StatusEntities.java │ │ │ │ └── StatusUpdate.java │ │ │ ├── tools │ │ │ └── LogLines.java │ │ │ ├── ui │ │ │ ├── activity │ │ │ │ ├── SearchActivity.java │ │ │ │ ├── SplashActivity.java │ │ │ │ ├── TweetHarvestingActivity.java │ │ │ │ └── TweetPostingActivity.java │ │ │ ├── fragment │ │ │ │ ├── SearchCategoryFragment.java │ │ │ │ ├── SearchFragment.java │ │ │ │ ├── TweetHarvestingFragment.java │ │ │ │ └── TweetPostingFragment.java │ │ │ └── suggestion │ │ │ │ ├── SuggestActivity.java │ │ │ │ ├── SuggestContract.java │ │ │ │ ├── SuggestFragment.java │ │ │ │ └── SuggestPresenter.java │ │ │ └── utility │ │ │ ├── Constants.java │ │ │ ├── FileUtils.java │ │ │ └── SharedPrefUtil.java │ └── res │ │ ├── anim │ │ ├── back_button_bottom_exit.xml │ │ ├── back_button_enter.xml │ │ ├── back_button_exit.xml │ │ ├── back_button_top_enter.xml │ │ ├── bottom_enter.xml │ │ ├── enter.xml │ │ ├── exit.xml │ │ └── top_exit.xml │ │ ├── drawable │ │ ├── camera.xml │ │ ├── heart.xml │ │ ├── ic_add_location_24dp.xml │ │ ├── ic_arrow_back_24dp.xml │ │ ├── ic_close_24dp.xml │ │ ├── ic_close_black_24dp.xml │ │ ├── ic_close_white.xml │ │ ├── ic_gallery_24dp.xml │ │ ├── ic_location_on_24dp.xml │ │ ├── ic_search_24dp.xml │ │ ├── loklak_splash.png │ │ ├── media_button_selector.xml │ │ ├── reply.xml │ │ ├── retweet.xml │ │ ├── shape_round_corner_tweet_media_remove.xml │ │ ├── shape_round_corner_tweet_post.xml │ │ ├── tweet_button_selector.xml │ │ ├── twitter_pink.xml │ │ └── twitter_white.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_search.xml │ │ ├── activity_splash.xml │ │ ├── activity_suggest.xml │ │ ├── activity_tweet_harvesting.xml │ │ ├── activity_tweet_posting.xml │ │ ├── content_tweet_harvesting.xml │ │ ├── fab_tweet_posting.xml │ │ ├── fragment_search.xml │ │ ├── fragment_search_category.xml │ │ ├── fragment_suggest.xml │ │ ├── fragment_tweet_harvesting.xml │ │ ├── fragment_tweet_posting.xml │ │ ├── grid_elem_tweet_photo.xml │ │ ├── item_post_tweet_image.xml │ │ ├── row_harvested_tweet.xml │ │ ├── row_tweet_search.xml │ │ └── row_tweet_search_suggest.xml │ │ ├── menu │ │ ├── menu_tweet_harvesting.xml │ │ └── menu_tweet_posting.xml │ │ ├── mipmap-hdpi │ │ └── launcher.png │ │ ├── mipmap-mdpi │ │ └── launcher.png │ │ ├── mipmap-xhdpi │ │ └── launcher.png │ │ ├── mipmap-xxhdpi │ │ └── launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── launcher.png │ │ ├── raw │ │ └── twitter.js │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── file_paths.xml │ └── test │ └── java │ └── org │ └── loklak │ └── wok │ ├── TwitterOAuthInterceptorTest.java │ └── UrlEscapeUtilsTest.java ├── build.gradle ├── docs └── _static │ ├── tweet_harvesting.png │ ├── tweet_post_authorization.png │ ├── tweet_posting.png │ ├── tweet_search.png │ ├── tweet_search_suggestions.png │ └── twitter_authorization_login.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── scripts ├── prep-key.sh └── secrets.tar.enc ├── settings.gradle └── update-apk.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = false 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespaces = true 10 | 11 | [{*.java, *.xml}] 12 | indent_style = space 13 | indent_size = 4 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespaces = false 18 | 19 | [*.json] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | [{*.yml,*.yaml}] 24 | indent_style = space 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | **Android version and Phone Model** 2 | 3 | Add the Android version and the phone model on which the issue was encountered. 4 | 5 | **Actual Behaviour** 6 | 7 | Please state here what is currently happening. 8 | 9 | **Expected Behaviour** 10 | 11 | State here what the feature should enable the user to do. 12 | 13 | **Steps to reproduce it** 14 | 15 | Add steps to reproduce bugs or add information on the place where the feature should be implemented. Add links to a sample deployment or code. 16 | 17 | **LogCat for the issue** 18 | 19 | Provide logs for the crash here 20 | 21 | **Screenshots of the issue** 22 | 23 | Where-ever possible attach a screenshot of the issue. 24 | 25 | **Would you like to work on the issue?** 26 | 27 | Please let us know if you can work on it or the issue should be assigned to someone else. 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | Fix #[Add issue number here. If you do not solve the issue entirely, please change the message e.g. "First steps for issues #IssueNumber] 2 | 3 | Changes: [Add here what changes were made in this issue and if possible provide links.] 4 | 5 | Screenshots for the change: 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | .DS_Store 4 | /build 5 | /captures 6 | .idea 7 | *.iml 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | sudo: required 4 | 5 | env: 6 | global: 7 | - GRADLE_OPTS="-Xmx512m" 8 | matrix: 9 | - ADB_INSTALL_TIMEOUT=15 10 | 11 | cache: 12 | directories: 13 | - "${TRAVIS_BUILD_DIR}/android/gradle/caches/" 14 | - "${TRAVIS_BUILD_DIR}/android/gradle/wrapper/dists/" 15 | - "$HOME/android/.gradle/caches/" 16 | - "$HOME/android/.gradle/wrapper/" 17 | 18 | android: 19 | components: 20 | - tools 21 | - platform-tools 22 | - build-tools-26.0.2 23 | - tools 24 | - android-25 25 | - android-22 26 | - addon-google_apis-google-25 27 | - extra-google-m2repository 28 | - extra-android-m2repository 29 | - android-sdk-license-.+ 30 | 31 | # system images for emulator 32 | - sys-img-armeabi-v7a-android-22 33 | 34 | before_script: 35 | - chmod +x gradlew 36 | - echo no | android create avd --force --name test --target android-22 --abi armeabi-v7a 37 | - emulator -avd test -no-skin -no-audio -no-window & 38 | 39 | script: 40 | - android-wait-for-emulator 41 | - adb shell input keyevent 82 & 42 | - ./gradlew connectedCheck test jacocoTestReport 43 | - ./gradlew build 44 | - ./gradlew assembleRelease 45 | - bash scripts/prep-key.sh 46 | # Success or not if a pull request is merged the apk should be updated if it can be build 47 | - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./update-apk.sh; fi' 48 | 49 | after_success: 50 | - bash <(curl -s https://codecov.io/bash) 51 | 52 | notifications: 53 | webhooks: 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loklak Wok for Android 2 | 3 | [![Build Status](https://travis-ci.org/fossasia/loklak_wok_android.svg?branch=master)](https://travis-ci.org/fossasia/loklak_wok_android) 4 | [![codecov.io](https://codecov.io/github/fossasia/loklak_wok_android/coverage.svg)](https://codecov.io/github/fossasia/loklak_wok_android) 5 | 6 | Loklak Wok Android is a message harvesting peer for the loklak_server. 7 | 8 | Users can also search tweets from the app, the displayed tweets are the latest tweets, tweets containing images and videos. Along with that, the app provides a tweet posting feature. Users can directly post the tweet from the app. Not only text, but images can also be tweeted from the app. 9 | 10 | ### Screenshots of the app 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | ## Communication 25 | Please join our mailing list to discuss questions regarding the project: https://groups.google.com/forum/#!forum/opntec-dev 26 | 27 | Our chat channel is on gitter here: https://gitter.im/loklak/loklak. 28 | 29 | ## Development Environment Setup 30 | Clone the repository 31 | ``` 32 | $ git clone https://github.com/fossasia/loklak_wok_android.git 33 | ``` 34 | Before you begin, you should already have the Android Studio SDK downloaded and set up correctly. You can find a guide on how to do this here: [Setting up Android Studio](http://developer.android.com/sdk/installing/index.html?pkg=studio) 35 | 36 | ### Setting up the Android Project 37 | 38 | 1. Download the [_loklak_wok_android_ project source](https://github.com/fossasia/loklak_wok_android). You can do this either by forking and cloning the repository (recommended if you plan on pushing changes) or by downloading it as a ZIP file and extracting it. 39 | 40 | 2. Open Android Studio, you will see a **Welcome to Android** window. Under Quick Start, select _Import Project (Eclipse ADT, Gradle, etc.)_ 41 | 42 | 3. Navigate to the directory where you saved the loklak_wok_android project, select this folder. 43 | 44 | 4. Once this process is complete and Android Studio opens, check the Console for any build errors. 45 | _Note:_ If you receive a Gradle sync error titled, "failed to find ...", you should click on the link below the error message (if available) that says _Install missing platform(s) and sync project_ and allow Android studio to fetch you what is missing. 46 | 47 | 5. Once all build errors have been resolved, you should be all set to build the app and test it. 48 | 49 | 6. To Build the app, go to _Build>Make Project_ (or alternatively press the Make Project icon in the toolbar). 50 | 51 | 7. If the app was built successfully, you can test it by running it on either a real device or an emulated one by going to _Run>Run 'app'_ or pressing the Run icon in the toolbar. 52 | 53 | ## Libraries Used 54 | * Retrofit [docs](http://square.github.io/retrofit/2.x/retrofit/) 55 | * Gson [docs](http://www.javadoc.io/doc/com.google.code.gson/gson/2.8.1) 56 | * ButterKnife [docs](http://jakewharton.github.io/butterknife/javadoc/) 57 | * RxJava [docs](http://reactivex.io/RxJava/javadoc/) 58 | * RxAndroid [docs](https://www.javadoc.io/doc/io.reactivex/rxandroid/1.2.1) 59 | * LiquidCore [docs](https://liquidplayer.github.io/LiquidCoreAndroid/0.2.2/) 60 | * Glide [docs](http://bumptech.github.io/glide/javadocs/images/360/index.html) 61 | 62 | The project uses **lambda expressions** and **RxJava** heavily. So, if you are new to these, the 63 | following resources would be helpful to get you started: 64 | * Lambda Expressions 65 | * [Jenkov Tutorials](http://tutorials.jenkov.com/java/lambda-expressions.html) for a quick start. 66 | * [Java Brains](https://www.youtube.com/playlist?list=PLqq-6Pq4lTTa9YGfyhyW2CqdtW9RtY-I3) youtube 67 | series for a detail understanding. 68 | * RxJava 69 | * [Grokking RxJava](http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/) series by Dan Lew. 70 | * Code Tutplus RxJava series ([Introduction](https://code.tutsplus.com/tutorials/getting-started-with-rxjava-20-for-android--cms-28345), 71 | [Operators](https://code.tutsplus.com/tutorials/reactive-programming-operators-in-rxjava-20--cms-28396) 72 | and [Use in Android](https://code.tutsplus.com/tutorials/rxjava-for-android-apps-introducing-rxbinding-and-rxlifecycle--cms-28565)) by Jessica Thornsby. 73 | 74 | ## Contributions, Bug Reports and Feature Requests 75 | This is an Open Source project and we would be happy to see contributors who report bugs and file 76 | feature requests submitting pull requests as well. Please report issues here 77 | https://github.com/fossasia/loklak_wok_android/issues. 78 | 79 | ## Branch Policy 80 | * **master** All development goes on in this branch. If you're making a contribution, you are 81 | supposed to make a pull request to master. PRs to master must pass a build check on Travis. 82 | * **apk** This branch contains apks, that are automatically generated on merging the latest pull request. 83 | 84 | ## Code Style Guidelines 85 | For contributions please read the [CODESTYLE](https://source.android.com/source/code-style) carefully. 86 | An additional rule: Maximum 100 characters per line. 87 | 88 | Try to remove as many warnings (yellow markings on the right side of Android Studio) as possible, 89 | It's not completely possible to remove all the warnings, but over a period of time, we should try to 90 | make it as complete as possible. 91 | 92 | ## LICENSE 93 | This is licensed under LGPL 2.1. 94 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | // TODO: Uncomment to enable crashlytics 3 | // apply plugin: 'io.fabric' 4 | apply plugin: 'realm-android' 5 | apply plugin: 'jacoco-android' 6 | 7 | repositories { 8 | maven { url 'https://maven.fabric.io/public' } 9 | } 10 | 11 | android { 12 | compileSdkVersion 25 13 | buildToolsVersion "26.0.2" 14 | 15 | defaultConfig { 16 | applicationId "org.loklak.wok" 17 | minSdkVersion 18 18 | targetSdkVersion 25 19 | versionCode 1 20 | versionName "1" 21 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 22 | vectorDrawables.useSupportLibrary = true 23 | } 24 | buildTypes { 25 | release { 26 | minifyEnabled false 27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | 31 | // This is important, it will run lint checks but won't abort build 32 | lintOptions { 33 | abortOnError false 34 | } 35 | 36 | compileOptions { 37 | sourceCompatibility JavaVersion.VERSION_1_8 38 | targetCompatibility JavaVersion.VERSION_1_8 39 | } 40 | 41 | packagingOptions { 42 | exclude 'META-INF/rxjava.properties' 43 | } 44 | } 45 | 46 | dependencies { 47 | implementation fileTree(include: ['*.jar'], dir: 'libs') 48 | 49 | testImplementation 'junit:junit:4.12' 50 | testImplementation 'org.assertj:assertj-core:1.7.1' 51 | testImplementation 'org.mockito:mockito-core:2.8.47' 52 | testImplementation 'com.android.support:support-annotations:25.3.1' 53 | 54 | androidTestImplementation 'org.mockito:mockito-android:2.8.47' 55 | androidTestImplementation 'com.android.support:support-annotations:25.3.1' 56 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2' 57 | 58 | implementation 'com.android.support:appcompat-v7:25.3.1' 59 | implementation 'com.android.support:support-v4:25.3.1' 60 | implementation 'com.android.support:design:25.3.1' 61 | implementation 'com.android.support:cardview-v7:25.3.1' 62 | 63 | implementation 'org.joda:joda-convert:1.8' 64 | implementation 'joda-time:joda-time:2.9.4' 65 | 66 | implementation 'com.google.code.gson:gson:2.8.1' 67 | 68 | implementation 'com.squareup.retrofit2:retrofit:2.3.0' 69 | implementation 'com.squareup.retrofit2:converter-gson:2.3.0' 70 | implementation 'com.squareup.okhttp3:logging-interceptor:3.7.0' 71 | implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' 72 | 73 | implementation 'com.jakewharton:butterknife:8.6.0' 74 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0' 75 | 76 | implementation 'com.google.dagger:dagger:2.11' 77 | annotationProcessor 'com.google.dagger:dagger-compiler:2.11' 78 | 79 | implementation 'io.reactivex.rxjava2:rxjava:2.0.5' 80 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' 81 | implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' 82 | 83 | implementation 'com.github.LiquidPlayer:LiquidCore:0.2.2' 84 | 85 | implementation 'com.github.bumptech.glide:glide:3.7.0' 86 | 87 | implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') { 88 | transitive = true; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/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/admin/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 | -------------------------------------------------------------------------------- /app/src/androidTest/java/org/loklak/wok/SuggestPresenterTest.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok; 2 | 3 | 4 | import android.support.test.InstrumentationRegistry; 5 | 6 | import org.junit.After; 7 | import org.junit.AfterClass; 8 | import org.junit.Before; 9 | import org.junit.BeforeClass; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.loklak.wok.api.loklak.LoklakAPI; 13 | import org.loklak.wok.model.suggest.Query; 14 | import org.loklak.wok.model.suggest.SuggestData; 15 | import org.loklak.wok.ui.suggestion.SuggestContract; 16 | import org.loklak.wok.ui.suggestion.SuggestPresenter; 17 | import org.mockito.junit.MockitoJUnitRunner; 18 | 19 | import java.io.IOException; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | import io.reactivex.Observable; 24 | import io.reactivex.android.plugins.RxAndroidPlugins; 25 | import io.reactivex.plugins.RxJavaPlugins; 26 | import io.reactivex.schedulers.Schedulers; 27 | import io.realm.Realm; 28 | import io.realm.RealmConfiguration; 29 | 30 | import static org.junit.Assert.assertEquals; 31 | import static org.mockito.ArgumentMatchers.anyString; 32 | import static org.mockito.Mockito.mock; 33 | import static org.mockito.Mockito.verify; 34 | import static org.mockito.Mockito.when; 35 | 36 | @RunWith(MockitoJUnitRunner.class) 37 | public class SuggestPresenterTest { 38 | 39 | private SuggestContract.View mMockView; 40 | private SuggestPresenter mPresenter; 41 | private LoklakAPI mApi; 42 | private List queries; 43 | private static Realm mDb; 44 | 45 | @BeforeClass 46 | public static void setDb() { 47 | Realm.init(InstrumentationRegistry.getContext()); 48 | RealmConfiguration testConfig = new RealmConfiguration.Builder() 49 | .inMemory() 50 | .name("test-db") 51 | .build(); 52 | mDb = Realm.getInstance(testConfig); 53 | } 54 | 55 | @AfterClass 56 | public static void closeDb() { 57 | mDb.close(); 58 | } 59 | 60 | @Before 61 | public void setUp() throws Exception { 62 | mMockView = mock(SuggestContract.View.class); 63 | mApi = mock(LoklakAPI.class); 64 | 65 | mPresenter = new SuggestPresenter(mApi, mDb); 66 | mPresenter.attachView(mMockView); 67 | 68 | queries = getFakeQueries(); 69 | 70 | RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline()); 71 | RxAndroidPlugins.setMainThreadSchedulerHandler(scheduler -> Schedulers.trampoline()); 72 | 73 | mDb.beginTransaction(); 74 | mDb.copyToRealm(queries); 75 | mDb.commitTransaction(); 76 | } 77 | 78 | @After 79 | public void tearDown() throws Exception { 80 | mPresenter.detachView(); 81 | } 82 | 83 | private List getFakeQueries() { 84 | List queryList = new ArrayList<>(); 85 | 86 | Query linux = new Query(); 87 | linux.setQuery("linux"); 88 | queryList.add(linux); 89 | 90 | Query india = new Query(); 91 | india.setQuery("india"); 92 | queryList.add(india); 93 | 94 | return queryList; 95 | } 96 | 97 | private Observable getFakeSuggestions() { 98 | SuggestData suggestData = new SuggestData(); 99 | suggestData.setQueries(queries); 100 | return Observable.just(suggestData); 101 | } 102 | 103 | private void stubSuggestionsFromApi(Observable observable) { 104 | when(mApi.getSuggestions(anyString())).thenReturn(observable); 105 | } 106 | 107 | @Test 108 | public void testLoadSuggestionsFromApi() { 109 | stubSuggestionsFromApi(getFakeSuggestions()); 110 | 111 | mPresenter.loadSuggestionsFromAPI("", true); 112 | 113 | verify(mMockView).showProgressBar(true); 114 | verify(mMockView).onSuggestionFetchSuccessful(queries); 115 | verify(mMockView).showProgressBar(false); 116 | } 117 | 118 | @Test 119 | public void testLoadSuggestionsFromApiFail() { 120 | Throwable throwable = new IOException(); 121 | stubSuggestionsFromApi(Observable.error(throwable)); 122 | 123 | mPresenter.loadSuggestionsFromAPI("", true); 124 | verify(mMockView).showProgressBar(true); 125 | verify(mMockView).showProgressBar(false); 126 | verify(mMockView).onSuggestionFetchError(throwable); 127 | } 128 | 129 | @Test 130 | public void testSaveSuggestions() { 131 | mPresenter.saveSuggestions(queries); 132 | int count = mDb.where(Query.class).findAll().size(); 133 | assertEquals(queries.size(), count); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 28 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 49 | 50 | 53 | 54 | 58 | 59 | 61 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/Constants.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok; 2 | 3 | 4 | public class Constants { 5 | 6 | public static final String TWEET_SEARCH_SUGGESTION_QUERY_KEY = "search_suggestion_query"; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/LoklakWokApplication.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok; 2 | 3 | import android.app.Application; 4 | 5 | import org.loklak.wok.inject.ApplicationComponent; 6 | import org.loklak.wok.inject.DaggerApplicationComponent; 7 | import org.loklak.wok.inject.ApplicationModule; 8 | import org.loklak.wok.utility.Constants; 9 | 10 | import io.realm.Realm; 11 | import io.realm.RealmConfiguration; 12 | 13 | 14 | public class LoklakWokApplication extends Application { 15 | 16 | private ApplicationComponent mApplicationComponent; 17 | 18 | @Override 19 | public void onCreate() { 20 | super.onCreate(); 21 | // TODO: uncomment the following and provide API key in manifest file to enable Crashlytics 22 | // Fabric.with(this, new Crashlytics()); 23 | 24 | Realm.init(this); 25 | RealmConfiguration realmConfiguration = new RealmConfiguration.Builder() 26 | .name(Realm.DEFAULT_REALM_NAME) 27 | .deleteRealmIfMigrationNeeded() 28 | .build(); 29 | Realm.setDefaultConfiguration(realmConfiguration); 30 | 31 | mApplicationComponent = DaggerApplicationComponent.builder() 32 | .applicationModule(new ApplicationModule(Constants.BASE_URL_LOKLAK)) 33 | .build(); 34 | } 35 | 36 | public ApplicationComponent getApplicationComponent() { 37 | return mApplicationComponent; 38 | } 39 | 40 | @Override 41 | public void onTerminate() { 42 | Realm.getDefaultInstance().close(); 43 | super.onTerminate(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/Utility.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok; 2 | 3 | 4 | import android.content.Context; 5 | import android.widget.Toast; 6 | 7 | import com.google.gson.FieldNamingPolicy; 8 | import com.google.gson.Gson; 9 | import com.google.gson.GsonBuilder; 10 | 11 | public class Utility { 12 | 13 | public static Gson getGsonForPrivateVariableClass() { 14 | return new GsonBuilder().setFieldNamingStrategy(field -> { 15 | String name = FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES.translateName(field); 16 | return name.substring(2); // private fields are named as mName i.e m_name 17 | }).create(); 18 | } 19 | 20 | public static void displayToast(Toast toast, Context context, String toastMessage) { 21 | if (toast != null) toast.cancel(); 22 | toast = Toast.makeText(context, toastMessage, Toast.LENGTH_SHORT); 23 | toast.show(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/adapters/HarvestedTweetAdapter.java: -------------------------------------------------------------------------------- 1 | 2 | package org.loklak.wok.adapters; 3 | 4 | import android.support.v7.widget.RecyclerView; 5 | import android.text.format.DateUtils; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.TextView; 10 | 11 | import org.loklak.wok.model.harvest.Status; 12 | import org.loklak.wok.model.harvest.User; 13 | import org.loklak.wok.R; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import butterknife.BindView; 19 | import butterknife.ButterKnife; 20 | 21 | public class HarvestedTweetAdapter 22 | extends RecyclerView.Adapter { 23 | 24 | private List mHarvestedTweetList; 25 | 26 | public HarvestedTweetAdapter(List harvestedTweets) { 27 | this.mHarvestedTweetList = harvestedTweets; 28 | } 29 | 30 | @Override 31 | public HarvestedTweetViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 32 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 33 | View view = inflater.inflate(R.layout.row_harvested_tweet, parent, false); 34 | return new HarvestedTweetViewHolder(view); 35 | } 36 | 37 | @Override 38 | public void onBindViewHolder(HarvestedTweetViewHolder holder, int position) { 39 | holder.bind(mHarvestedTweetList.get(position)); 40 | } 41 | 42 | @Override 43 | public int getItemCount() { 44 | return mHarvestedTweetList.size(); 45 | } 46 | 47 | public void addHarvestedTweets(List harvestedTweetList) { 48 | int count = mHarvestedTweetList.size(); 49 | mHarvestedTweetList.addAll(harvestedTweetList); 50 | notifyItemRangeInserted(getItemCount(), count); 51 | } 52 | 53 | public ArrayList getHarvestedTweetList() { 54 | return (ArrayList) mHarvestedTweetList; 55 | } 56 | 57 | public void clearAdapter() { 58 | mHarvestedTweetList.clear(); 59 | notifyDataSetChanged(); 60 | } 61 | 62 | class HarvestedTweetViewHolder extends RecyclerView.ViewHolder { 63 | 64 | @BindView(R.id.user_fullname) 65 | TextView userFullname; 66 | @BindView(R.id.username) 67 | TextView username; 68 | @BindView(R.id.tweet_date) 69 | TextView tweetDate; 70 | @BindView(R.id.harvested_tweet_text) 71 | TextView harvestedTweetTextView; 72 | 73 | public HarvestedTweetViewHolder(View itemView) { 74 | super(itemView); 75 | ButterKnife.bind(this, itemView); 76 | } 77 | 78 | void bind(Status harvestedTweet) { 79 | User user = harvestedTweet.getUser(); 80 | userFullname.setText(user.getName()); 81 | username.setText("@" + user.getScreenName()); 82 | tweetDate.setText(getReadableDate(harvestedTweet.getCreatedAt())); 83 | harvestedTweetTextView.setText(harvestedTweet.getText()); 84 | } 85 | 86 | private String getReadableDate(Long miliseconds) { 87 | 88 | CharSequence formatted = DateUtils.getRelativeTimeSpanString(miliseconds); 89 | return formatted.toString(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/adapters/PostTweetMediaAdapter.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.adapters; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageButton; 11 | import android.widget.ImageView; 12 | 13 | import org.loklak.wok.R; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import butterknife.BindView; 19 | import butterknife.ButterKnife; 20 | import butterknife.OnClick; 21 | 22 | 23 | public class PostTweetMediaAdapter 24 | extends RecyclerView.Adapter { 25 | 26 | private final String LOG_TAG = PostTweetMediaAdapter.class.getName(); 27 | 28 | private Context mContext; 29 | private List mImagePathList = new ArrayList<>(); 30 | 31 | public PostTweetMediaAdapter(Context context, List imagePaths) { 32 | this.mContext = context; 33 | this.mImagePathList = imagePaths; 34 | } 35 | 36 | @Override 37 | public PostTweetMediaViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 38 | LayoutInflater inflater = LayoutInflater.from(mContext); 39 | View view = inflater.inflate(R.layout.item_post_tweet_image, parent, false); 40 | return new PostTweetMediaViewHolder(view); 41 | } 42 | 43 | @Override 44 | public void onBindViewHolder(PostTweetMediaViewHolder holder, int position) { 45 | holder.bind(mImagePathList.get(position)); 46 | } 47 | 48 | @Override 49 | public int getItemCount() { 50 | return mImagePathList.size(); 51 | } 52 | 53 | public ArrayList getImagePathList() { 54 | return (ArrayList) mImagePathList; 55 | } 56 | 57 | public void setImagePathList(List imagePaths) { 58 | this.mImagePathList = imagePaths; 59 | notifyDataSetChanged(); 60 | } 61 | 62 | public void addImagePath(String imagePath) { 63 | mImagePathList.add(imagePath); 64 | notifyDataSetChanged(); 65 | } 66 | 67 | public void clearAdapter() { 68 | mImagePathList.clear(); 69 | notifyDataSetChanged(); 70 | } 71 | 72 | class PostTweetMediaViewHolder extends RecyclerView.ViewHolder { 73 | 74 | private final String LOG_TAG = PostTweetMediaViewHolder.class.getName(); 75 | 76 | @BindView(R.id.tweet_media) 77 | ImageView tweetMedia; 78 | @BindView(R.id.tweet_media_remove) 79 | ImageButton tweetMediaRemove; 80 | 81 | PostTweetMediaViewHolder(View itemView) { 82 | super(itemView); 83 | ButterKnife.bind(this, itemView); 84 | } 85 | 86 | void bind(String imagePath) { 87 | Bitmap bitmap = BitmapFactory.decodeFile(imagePath); 88 | tweetMedia.setImageBitmap(bitmap); 89 | } 90 | 91 | @OnClick(R.id.tweet_media_remove) 92 | public void onClickTweetMediaRemove() { 93 | int position = getAdapterPosition(); 94 | mImagePathList.remove(position); 95 | notifyItemRemoved(position); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/adapters/SearchCategoryAdapter.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.adapters; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.support.v7.widget.StaggeredGridLayoutManager; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import com.bumptech.glide.Glide; 13 | 14 | import org.loklak.wok.model.search.Status; 15 | import org.loklak.wok.model.search.User; 16 | import org.loklak.wok.R; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import butterknife.BindView; 22 | import butterknife.ButterKnife; 23 | 24 | public class SearchCategoryAdapter 25 | extends RecyclerView.Adapter { 26 | 27 | private Context mContext; 28 | private List mStatuses; 29 | 30 | public SearchCategoryAdapter(Context context, List statuses) { 31 | this.mContext = context; 32 | this.mStatuses = statuses; 33 | } 34 | 35 | @Override 36 | public SearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 37 | View view = LayoutInflater.from(mContext).inflate(R.layout.row_tweet_search, parent, false); 38 | return new SearchViewHolder(view); 39 | } 40 | 41 | @Override 42 | public void onBindViewHolder(SearchViewHolder holder, int position) { 43 | holder.bind(mStatuses.get(position)); 44 | } 45 | 46 | @Override 47 | public int getItemCount() { 48 | return mStatuses.size(); 49 | } 50 | 51 | public ArrayList getStatuses() { 52 | return (ArrayList) mStatuses; 53 | } 54 | 55 | public void setStatuses(List statuses) { 56 | mStatuses = statuses; 57 | notifyDataSetChanged(); 58 | } 59 | 60 | class SearchViewHolder extends RecyclerView.ViewHolder { 61 | 62 | @BindView(R.id.user_profile_pic) 63 | ImageView userProfilePic; 64 | @BindView(R.id.user_fullname) 65 | TextView userFullname; 66 | @BindView(R.id.tweet_date) 67 | TextView tweetDate; 68 | @BindView(R.id.username) 69 | TextView username; 70 | @BindView(R.id.tweet_text) 71 | TextView tweetText; 72 | @BindView(R.id.tweet_photos) 73 | RecyclerView tweetPhotos; 74 | @BindView(R.id.number_retweets) 75 | TextView numberRetweets; 76 | @BindView(R.id.number_likes) 77 | TextView numberLikes; 78 | 79 | SearchViewHolder(View itemView) { 80 | super(itemView); 81 | ButterKnife.bind(this, itemView); 82 | } 83 | 84 | void bind(Status status) { 85 | User user = status.getUser(); 86 | 87 | Glide.with(mContext).load(user.getProfileImageUrlHttps()).into(userProfilePic); 88 | userFullname.setText(user.getName()); 89 | username.setText(user.getScreenName()); 90 | tweetText.setText(status.getText()); 91 | numberRetweets.setText(String.valueOf(status.getRetweetCount())); 92 | numberLikes.setText(String.valueOf(status.getFavouritesCount())); 93 | 94 | List imageUrls = filterImages(status.getImages()); 95 | int orientation = StaggeredGridLayoutManager.VERTICAL; 96 | StaggeredGridLayoutManager layoutManager; 97 | if (imageUrls.size() > 0) { 98 | tweetPhotos.setVisibility(View.VISIBLE); 99 | if (imageUrls.size() == 1) { 100 | layoutManager = new StaggeredGridLayoutManager(1, orientation); 101 | } else { 102 | layoutManager = new StaggeredGridLayoutManager(2, orientation); 103 | } 104 | layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); 105 | tweetPhotos.setLayoutManager(layoutManager); 106 | TweetImagesAdapter tweetImagesAdapter = new TweetImagesAdapter(mContext, imageUrls); 107 | tweetPhotos.setAdapter(tweetImagesAdapter); 108 | } else { 109 | tweetPhotos.setVisibility(View.GONE); 110 | } 111 | } 112 | 113 | private List filterImages(List imageUrls) { 114 | List onlyPbsImages = new ArrayList<>(); 115 | for (String url: imageUrls) { 116 | if (url.contains("pbs")) { 117 | onlyPbsImages.add(url); 118 | } 119 | } 120 | return onlyPbsImages; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/adapters/SearchFragmentPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.adapters; 2 | 3 | import android.support.v4.app.Fragment; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v4.app.FragmentPagerAdapter; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class SearchFragmentPagerAdapter extends FragmentPagerAdapter { 11 | 12 | private List mFragmentList = new ArrayList<>(); 13 | private List mFragmentNameList = new ArrayList<>(); 14 | 15 | public SearchFragmentPagerAdapter(FragmentManager fm) { 16 | super(fm); 17 | } 18 | 19 | @Override 20 | public Fragment getItem(int position) { 21 | return mFragmentList.get(position); 22 | } 23 | 24 | @Override 25 | public int getCount() { 26 | return mFragmentList.size(); 27 | } 28 | 29 | @Override 30 | public CharSequence getPageTitle(int position) { 31 | return mFragmentNameList.get(position); 32 | } 33 | 34 | public void addFragment(Fragment fragment, String pageTitle) { 35 | mFragmentList.add(fragment); 36 | mFragmentNameList.add(pageTitle); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/adapters/SuggestAdapter.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.adapters; 2 | 3 | 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.TextView; 9 | 10 | import org.loklak.wok.model.suggest.Query; 11 | import org.loklak.wok.R; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import butterknife.BindView; 17 | import butterknife.ButterKnife; 18 | 19 | 20 | public class SuggestAdapter extends RecyclerView.Adapter { 21 | 22 | private List mQueries = new ArrayList<>(); 23 | private OnSuggestionClickListener mSuggestionClickListener; 24 | 25 | public SuggestAdapter(List queries, OnSuggestionClickListener clickListener) { 26 | mQueries = queries; 27 | mSuggestionClickListener = clickListener; 28 | } 29 | 30 | public interface OnSuggestionClickListener { 31 | void onSuggestionClicked(Query query); 32 | } 33 | 34 | @Override 35 | public SuggestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 36 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 37 | View view = inflater.inflate(R.layout.row_tweet_search_suggest, parent, false); 38 | return new SuggestViewHolder(view); 39 | } 40 | 41 | @Override 42 | public void onBindViewHolder(SuggestViewHolder holder, int position) { 43 | holder.bind(mQueries.get(position)); 44 | } 45 | 46 | @Override 47 | public int getItemCount() { 48 | return mQueries.size(); 49 | } 50 | 51 | public void setQueries(List queries) { 52 | mQueries = queries; 53 | notifyDataSetChanged(); 54 | } 55 | 56 | public List getQueries() { 57 | return mQueries; 58 | } 59 | 60 | class SuggestViewHolder extends RecyclerView.ViewHolder { 61 | 62 | @BindView(R.id.suggest_query) 63 | TextView suggestQuery; 64 | 65 | SuggestViewHolder(View itemView) { 66 | super(itemView); 67 | ButterKnife.bind(this, itemView); 68 | itemView.setOnClickListener(view -> { 69 | int position = getLayoutPosition(); 70 | Query query = mQueries.get(position); 71 | mSuggestionClickListener.onSuggestionClicked(query); 72 | }); 73 | } 74 | 75 | void bind(Query query) { 76 | suggestQuery.setText(query.getQuery()); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/adapters/TweetImagesAdapter.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.adapters; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | 10 | import com.bumptech.glide.Glide; 11 | 12 | import org.loklak.wok.R; 13 | 14 | import java.util.List; 15 | 16 | import butterknife.BindView; 17 | import butterknife.ButterKnife; 18 | 19 | public class TweetImagesAdapter extends 20 | RecyclerView.Adapter { 21 | 22 | private Context mContext; 23 | private List mImageUrls; 24 | 25 | public TweetImagesAdapter(Context context, List imageUrls) { 26 | this.mContext = context; 27 | this.mImageUrls = imageUrls; 28 | } 29 | 30 | @Override 31 | public TweetImagesViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 32 | LayoutInflater layoutInflater = LayoutInflater.from(mContext); 33 | View view = layoutInflater.inflate(R.layout.grid_elem_tweet_photo, parent, false); 34 | return new TweetImagesViewHolder(view); 35 | } 36 | 37 | @Override 38 | public void onBindViewHolder(TweetImagesViewHolder holder, int position) { 39 | holder.bind(mImageUrls.get(position)); 40 | } 41 | 42 | @Override 43 | public int getItemCount() { 44 | return mImageUrls.size(); 45 | } 46 | 47 | class TweetImagesViewHolder extends RecyclerView.ViewHolder { 48 | 49 | @BindView(R.id.tweet_image) 50 | ImageView tweetImage; 51 | 52 | public TweetImagesViewHolder(View itemView) { 53 | super(itemView); 54 | ButterKnife.bind(this, itemView); 55 | } 56 | 57 | void bind(String imageUrl) { 58 | Glide.with(mContext).load(imageUrl).fitCenter().into(tweetImage); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/api/loklak/LoklakAPI.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.api.loklak; 2 | 3 | import org.loklak.wok.model.harvest.Push; 4 | import org.loklak.wok.model.search.Search; 5 | import org.loklak.wok.model.suggest.SuggestData; 6 | 7 | import io.reactivex.Observable; 8 | import retrofit2.http.Field; 9 | import retrofit2.http.FormUrlEncoded; 10 | import retrofit2.http.GET; 11 | import retrofit2.http.POST; 12 | import retrofit2.http.Query; 13 | 14 | 15 | public interface LoklakAPI { 16 | 17 | @GET("/api/suggest.json") 18 | Observable getSuggestions(@Query("q") String query); 19 | 20 | @GET("/api/suggest.json") 21 | Observable getSuggestions(@Query("q") String query, @Query("count") int count); 22 | 23 | @POST("/api/push.json") 24 | @FormUrlEncoded 25 | Observable pushTweetsToLoklak(@Field("data") String data); 26 | 27 | @GET("api/search.json") 28 | Observable getSearchedTweets( 29 | @Query("q") String query, 30 | @Query("filter") String filter, 31 | @Query("count") int count); 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/api/loklak/RestClient.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.api.loklak; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import org.loklak.wok.Utility; 6 | 7 | import retrofit2.Retrofit; 8 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 9 | import retrofit2.converter.gson.GsonConverterFactory; 10 | 11 | 12 | public class RestClient { 13 | 14 | private static final String BASE_URL = "https://api.loklak.org/"; 15 | 16 | private static Gson gson = Utility.getGsonForPrivateVariableClass(); 17 | private static Retrofit sRetrofit; 18 | 19 | private RestClient() { 20 | } 21 | 22 | private static void createRestClient() { 23 | sRetrofit = new Retrofit.Builder() 24 | .baseUrl(BASE_URL) 25 | .addConverterFactory(GsonConverterFactory.create(gson)) 26 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 27 | .build(); 28 | } 29 | 30 | private static Retrofit getRetrofitInstance() { 31 | if (sRetrofit == null) { 32 | createRestClient(); 33 | } 34 | return sRetrofit; 35 | } 36 | 37 | public static T createApi(Class apiInterface) { 38 | return getRetrofitInstance().create(apiInterface); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/api/twitter/TwitterAPI.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.api.twitter; 2 | 3 | 4 | import org.loklak.wok.model.twitter.AccountVerifyCredentials; 5 | import org.loklak.wok.model.twitter.StatusUpdate; 6 | 7 | import io.reactivex.Observable; 8 | import okhttp3.ResponseBody; 9 | import retrofit2.http.Field; 10 | import retrofit2.http.FormUrlEncoded; 11 | import retrofit2.http.GET; 12 | import retrofit2.http.POST; 13 | 14 | public interface TwitterAPI { 15 | 16 | String BASE_URL = "https://api.twitter.com/"; 17 | 18 | /** 19 | * Used to obtain the request token, the first step in 3-legged authorization. 20 | * For more, please refer to: https://dev.twitter.com/web/sign-in/implementing 21 | * API doc link: https://dev.twitter.com/oauth/reference/post/oauth/request_token 22 | * @param oauthCallback The callback url for redirecting, after user allows the client app. 23 | * @return 24 | */ 25 | @FormUrlEncoded 26 | @POST("/oauth/request_token") 27 | Observable getRequestToken(@Field("oauth_callback") String oauthCallback); 28 | 29 | /** 30 | * Oauth access token and access token secret are fetched using the oauth_verifier obtained in 31 | * step 2 of 3-legged authorization for calling other twitter API endpoints. 32 | * For more, please refer to: https://dev.twitter.com/web/sign-in/implementing 33 | * API dock link: https://dev.twitter.com/oauth/reference/post/oauth/access_token 34 | * @param oauthVerifier 35 | * @return 36 | */ 37 | @FormUrlEncoded 38 | @POST("/oauth/access_token") 39 | Observable getAccessTokenAndSecret(@Field("oauth_verifier") String oauthVerifier); 40 | 41 | /** 42 | * User account details like user's full name, username, userid, profile image url are fetched. 43 | * API doc link: https://dev.twitter.com/rest/reference/get/account/verify_credentials 44 | * @return 45 | */ 46 | @GET("/1.1/account/verify_credentials.json") 47 | Observable getAccountCredentials(); 48 | 49 | /** 50 | * Used to post tweet. 51 | * API doc link: https://dev.twitter.com/rest/reference/post/statuses/update 52 | * @param status Text of the tweet 53 | * @param mediaIds Image ids, to be posted along with text. 54 | * @param latitude Latitude of user's location 55 | * @param longitude Longitude of user's location 56 | * @return 57 | */ 58 | @FormUrlEncoded 59 | @POST("/1.1/statuses/update.json") 60 | Observable postTweet( 61 | @Field("status") String status, 62 | @Field("media_ids") String mediaIds, 63 | @Field("lat") Double latitude, 64 | @Field("long") Double longitude); 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/api/twitter/TwitterMediaAPI.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.api.twitter; 2 | 3 | 4 | import org.loklak.wok.model.twitter.MediaUpload; 5 | 6 | import io.reactivex.Observable; 7 | import okhttp3.RequestBody; 8 | import retrofit2.http.Multipart; 9 | import retrofit2.http.POST; 10 | import retrofit2.http.Part; 11 | 12 | public interface TwitterMediaAPI { 13 | 14 | String BASE_URL = "https://upload.twitter.com/"; 15 | 16 | /** 17 | * Sends a multipart POST request, to obtain the image id, which can be passed as 18 | * mediaIds parameter for posting tweet with images, in 19 | * {@link TwitterAPI#postTweet(String, String, Double, Double)} 20 | * method. 21 | * @param rawBinary Raw binary of image file. 22 | * @param mediaData Base64 encoded string of image file. 23 | * @return 24 | */ 25 | @Multipart 26 | @POST("/1.1/media/upload.json") 27 | Observable getMediaId( 28 | @Part("media") RequestBody rawBinary, 29 | @Part("media_data") RequestBody mediaData 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/api/twitter/TwitterMediaRestClient.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.api.twitter; 2 | 3 | 4 | import org.loklak.wok.utility.Constants; 5 | 6 | import java.util.Random; 7 | 8 | import okhttp3.OkHttpClient; 9 | import okhttp3.logging.HttpLoggingInterceptor; 10 | import retrofit2.Retrofit; 11 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 12 | import retrofit2.converter.gson.GsonConverterFactory; 13 | 14 | public class TwitterMediaRestClient { 15 | 16 | private static TwitterOAuthInterceptor.Builder sMediaOAuthInterceptor = 17 | new TwitterOAuthInterceptor.Builder() 18 | .consumerKey(Constants.KEY) 19 | .consumerSecret(Constants.SECRET) 20 | .random(new Random()) 21 | .clock(new TwitterOAuthInterceptor.Clock()) 22 | .onlyOauthParams(true); 23 | 24 | public static TwitterMediaAPI createTwitterMediaAPI(String accessToken, String accessSecret) { 25 | HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); 26 | loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 27 | TwitterOAuthInterceptor mediaInterceptor = sMediaOAuthInterceptor 28 | .accessToken(accessToken) 29 | .accessSecret(accessSecret).build(); 30 | OkHttpClient okHttpClient = new OkHttpClient.Builder() 31 | .addInterceptor(mediaInterceptor) 32 | //.addInterceptor(loggingInterceptor) // uncomment to debug network requests 33 | .build(); 34 | Retrofit retrofit = new Retrofit.Builder() 35 | .baseUrl(TwitterMediaAPI.BASE_URL) 36 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 37 | .addConverterFactory(GsonConverterFactory.create()) 38 | .client(okHttpClient) 39 | .build(); 40 | return retrofit.create(TwitterMediaAPI.class); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/api/twitter/TwitterOAuthInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.api.twitter; 2 | 3 | /* 4 | * Copyright (C) 2015 Jake Wharton 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import java.io.IOException; 20 | import java.security.InvalidKeyException; 21 | import java.security.NoSuchAlgorithmException; 22 | import java.security.SecureRandom; 23 | import java.util.Map; 24 | import java.util.Random; 25 | import java.util.SortedMap; 26 | import java.util.TreeMap; 27 | 28 | import javax.crypto.Mac; 29 | import javax.crypto.spec.SecretKeySpec; 30 | 31 | import okhttp3.HttpUrl; 32 | import okhttp3.Interceptor; 33 | import okhttp3.Request; 34 | import okhttp3.RequestBody; 35 | import okhttp3.Response; 36 | import okio.Buffer; 37 | import okio.ByteString; 38 | 39 | public final class TwitterOAuthInterceptor implements Interceptor { 40 | private static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key"; 41 | private static final String OAUTH_NONCE = "oauth_nonce"; 42 | private static final String OAUTH_SIGNATURE = "oauth_signature"; 43 | private static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method"; 44 | private static final String OAUTH_SIGNATURE_METHOD_VALUE = "HMAC-SHA1"; 45 | private static final String OAUTH_TIMESTAMP = "oauth_timestamp"; 46 | private static final String OAUTH_ACCESS_TOKEN = "oauth_token"; 47 | private static final String OAUTH_VERSION = "oauth_version"; 48 | private static final String OAUTH_VERSION_VALUE = "1.0"; 49 | 50 | private final String consumerKey; 51 | private final String consumerSecret; 52 | private final String accessToken; 53 | private final String accessSecret; 54 | private final Random random; 55 | private final Clock clock; 56 | private Boolean onlyOauthParams; 57 | 58 | private TwitterOAuthInterceptor( 59 | String consumerKey, String consumerSecret, String accessToken, 60 | String accessSecret, Random random, Clock clock, Boolean onlyOauthParams) { 61 | this.consumerKey = consumerKey; 62 | this.consumerSecret = consumerSecret; 63 | this.accessToken = accessToken; 64 | this.accessSecret = accessSecret; 65 | this.random = random; 66 | this.clock = clock; 67 | if (onlyOauthParams == null) { 68 | this.onlyOauthParams = false; 69 | } else { 70 | this.onlyOauthParams = onlyOauthParams; 71 | } 72 | } 73 | 74 | @Override 75 | public Response intercept(Chain chain) throws IOException { 76 | return chain.proceed(signRequest(chain.request())); 77 | } 78 | 79 | public Request signRequest(Request request) throws IOException { 80 | byte[] nonce = new byte[32]; 81 | random.nextBytes(nonce); 82 | String oauthNonce = ByteString.of(nonce).base64().replaceAll("\\W", ""); 83 | String oauthTimestamp = clock.millis(); 84 | 85 | String consumerKeyValue = UrlEscapeUtils.escape(consumerKey); 86 | String accessTokenValue = UrlEscapeUtils.escape(accessToken); 87 | 88 | SortedMap parameters = new TreeMap<>(); 89 | parameters.put(OAUTH_CONSUMER_KEY, consumerKeyValue); 90 | parameters.put(OAUTH_ACCESS_TOKEN, accessTokenValue); 91 | parameters.put(OAUTH_NONCE, oauthNonce); 92 | parameters.put(OAUTH_TIMESTAMP, oauthTimestamp); 93 | parameters.put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE); 94 | parameters.put(OAUTH_VERSION, OAUTH_VERSION_VALUE); 95 | 96 | if (!onlyOauthParams) { 97 | HttpUrl url = request.url(); 98 | for (int i = 0; i < url.querySize(); i++) { 99 | parameters.put(UrlEscapeUtils.escape(url.queryParameterName(i)), 100 | UrlEscapeUtils.escape(url.queryParameterValue(i))); 101 | } 102 | 103 | Buffer body = new Buffer(); 104 | 105 | RequestBody requestBody = request.body(); 106 | if (requestBody != null) { 107 | requestBody.writeTo(body); 108 | } 109 | 110 | while (!body.exhausted()) { 111 | long keyEnd = body.indexOf((byte) '='); 112 | if (keyEnd == -1) 113 | throw new IllegalStateException("Key with no value: " + body.readUtf8()); 114 | String key = body.readUtf8(keyEnd); 115 | body.skip(1); // Equals. 116 | 117 | long valueEnd = body.indexOf((byte) '&'); 118 | String value = valueEnd == -1 ? body.readUtf8() : body.readUtf8(valueEnd); 119 | if (valueEnd != -1) body.skip(1); // Ampersand. 120 | 121 | parameters.put(key, value); 122 | } 123 | } 124 | 125 | Buffer base = new Buffer(); 126 | String method = request.method(); 127 | base.writeUtf8(method); 128 | base.writeByte('&'); 129 | base.writeUtf8( 130 | UrlEscapeUtils.escape(request.url().newBuilder().query(null).build().toString())); 131 | base.writeByte('&'); 132 | 133 | boolean first = true; 134 | for (Map.Entry entry : parameters.entrySet()) { 135 | if (!first) base.writeUtf8(UrlEscapeUtils.escape("&")); 136 | first = false; 137 | base.writeUtf8(UrlEscapeUtils.escape(entry.getKey())); 138 | base.writeUtf8(UrlEscapeUtils.escape("=")); 139 | base.writeUtf8(UrlEscapeUtils.escape(entry.getValue())); 140 | } 141 | 142 | String signingKey = 143 | UrlEscapeUtils.escape(consumerSecret) + "&" + UrlEscapeUtils.escape(accessSecret); 144 | 145 | SecretKeySpec keySpec = new SecretKeySpec(signingKey.getBytes(), "HmacSHA1"); 146 | Mac mac; 147 | try { 148 | mac = Mac.getInstance("HmacSHA1"); 149 | mac.init(keySpec); 150 | } catch (NoSuchAlgorithmException | InvalidKeyException e) { 151 | throw new IllegalStateException(e); 152 | } 153 | byte[] result = mac.doFinal(base.readByteArray()); 154 | String signature = ByteString.of(result).base64(); 155 | 156 | String authorization = 157 | "OAuth " + OAUTH_CONSUMER_KEY + "=\"" + consumerKeyValue + "\", " + OAUTH_NONCE + "=\"" 158 | + oauthNonce + "\", " + OAUTH_SIGNATURE + "=\"" + UrlEscapeUtils.escape(signature) 159 | + "\", " + OAUTH_SIGNATURE_METHOD + "=\"" + OAUTH_SIGNATURE_METHOD_VALUE + "\", " 160 | + OAUTH_TIMESTAMP + "=\"" + oauthTimestamp + "\", " + OAUTH_ACCESS_TOKEN + "=\"" 161 | + accessTokenValue + "\", " + OAUTH_VERSION + "=\"" + OAUTH_VERSION_VALUE + "\""; 162 | 163 | return request.newBuilder().addHeader("Authorization", authorization).build(); 164 | } 165 | 166 | public static final class Builder { 167 | private String consumerKey; 168 | private String consumerSecret; 169 | private String accessToken; 170 | private String accessSecret; 171 | private Random random = new SecureRandom(); 172 | private Clock clock = new Clock(); 173 | private Boolean onlyOauthParams; 174 | 175 | public Builder consumerKey(String consumerKey) { 176 | if (consumerKey == null) throw new NullPointerException("consumerKey = null"); 177 | this.consumerKey = consumerKey; 178 | return this; 179 | } 180 | 181 | public Builder consumerSecret(String consumerSecret) { 182 | if (consumerSecret == null) throw new NullPointerException("consumerSecret = null"); 183 | this.consumerSecret = consumerSecret; 184 | return this; 185 | } 186 | 187 | public Builder accessToken(String accessToken) { 188 | if (accessToken == null) throw new NullPointerException("accessToken == null"); 189 | this.accessToken = accessToken; 190 | return this; 191 | } 192 | 193 | public Builder accessSecret(String accessSecret) { 194 | if (accessSecret == null) throw new NullPointerException("accessSecret == null"); 195 | this.accessSecret = accessSecret; 196 | return this; 197 | } 198 | 199 | public Builder random(Random random) { 200 | if (random == null) throw new NullPointerException("random == null"); 201 | this.random = random; 202 | return this; 203 | } 204 | 205 | public Builder clock(Clock clock) { 206 | if (clock == null) throw new NullPointerException("clock == null"); 207 | this.clock = clock; 208 | return this; 209 | } 210 | 211 | public Builder onlyOauthParams(Boolean onlyOauthParams) { 212 | if (onlyOauthParams == null) this.onlyOauthParams = false; 213 | else this.onlyOauthParams = onlyOauthParams; 214 | return this; 215 | } 216 | 217 | public TwitterOAuthInterceptor build() { 218 | if (consumerKey == null) throw new IllegalStateException("consumerKey not set"); 219 | if (consumerSecret == null) throw new IllegalStateException("consumerSecret not set"); 220 | if (accessToken == null) throw new IllegalStateException("accessToken not set"); 221 | if (accessSecret == null) throw new IllegalStateException("accessSecret not set"); 222 | return new TwitterOAuthInterceptor(consumerKey, consumerSecret, accessToken, accessSecret, 223 | random, clock, onlyOauthParams); 224 | } 225 | } 226 | 227 | /** Simple clock like class, to allow time mocking. */ 228 | public static class Clock { 229 | /** Returns the current time in milliseconds divided by 1K. */ 230 | public String millis() { 231 | return Long.toString(System.currentTimeMillis() / 1000L); 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/api/twitter/TwitterRestClient.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.api.twitter; 2 | 3 | 4 | import com.google.gson.Gson; 5 | 6 | import org.loklak.wok.Utility; 7 | import org.loklak.wok.utility.Constants; 8 | 9 | import java.util.Random; 10 | 11 | import okhttp3.OkHttpClient; 12 | import okhttp3.logging.HttpLoggingInterceptor; 13 | import retrofit2.Retrofit; 14 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 15 | import retrofit2.converter.gson.GsonConverterFactory; 16 | 17 | public class TwitterRestClient { 18 | 19 | // basic builders created 20 | private static TwitterOAuthInterceptor.Builder sInterceptorBuilder = 21 | new TwitterOAuthInterceptor.Builder() 22 | .consumerKey(Constants.KEY) 23 | .consumerSecret(Constants.SECRET) 24 | .random(new Random()) 25 | .clock(new TwitterOAuthInterceptor.Clock()); 26 | 27 | private static Gson mGson = Utility.getGsonForPrivateVariableClass(); 28 | private static Retrofit.Builder sRetrofitBuilder = new Retrofit.Builder() 29 | .baseUrl(TwitterAPI.BASE_URL) 30 | .addConverterFactory(GsonConverterFactory.create(mGson)) 31 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()); 32 | 33 | // without access_token interceptor and client created 34 | private static TwitterOAuthInterceptor sWithoutAccessTokenInterceptor = 35 | sInterceptorBuilder.accessToken("").accessSecret("").build(); 36 | 37 | // logger for debugging 38 | private static HttpLoggingInterceptor sLoggingInterceptor = new HttpLoggingInterceptor(); 39 | 40 | private static OkHttpClient.Builder sWithoutAccessTokenClient = new OkHttpClient.Builder() 41 | .addInterceptor(sWithoutAccessTokenInterceptor); 42 | 43 | private static Retrofit sWithoutAccessTokenRetrofit; 44 | 45 | 46 | public static TwitterAPI createTwitterAPIWithoutAccessToken() { 47 | if (sWithoutAccessTokenRetrofit == null) { 48 | sLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 49 | // uncomment to debug network requests 50 | // sWithoutAccessTokenClient.addInterceptor(sLoggingInterceptor); 51 | sWithoutAccessTokenRetrofit = sRetrofitBuilder 52 | .client(sWithoutAccessTokenClient.build()).build(); 53 | } 54 | return sWithoutAccessTokenRetrofit.create(TwitterAPI.class); 55 | } 56 | 57 | public static TwitterAPI createTwitterAPIWithAccessToken(String token) { 58 | TwitterOAuthInterceptor withAccessTokenInterceptor = 59 | sInterceptorBuilder.accessToken(token).accessSecret("").build(); 60 | OkHttpClient withAccessTokenClient = new OkHttpClient.Builder() 61 | .addInterceptor(withAccessTokenInterceptor) 62 | //.addInterceptor(loggingInterceptor) // uncomment to debug network requests 63 | .build(); 64 | Retrofit withAccessTokenRetrofit = sRetrofitBuilder.client(withAccessTokenClient).build(); 65 | return withAccessTokenRetrofit.create(TwitterAPI.class); 66 | } 67 | 68 | public static TwitterAPI createTwitterAPIWithAccessTokenAndSecret( 69 | String token, String tokenSecret) { 70 | sLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 71 | TwitterOAuthInterceptor withAccessTokenAndSecretInterceptor = 72 | sInterceptorBuilder.accessToken(token).accessSecret(tokenSecret).build(); 73 | OkHttpClient withAccessTokenAndSecretClient = new OkHttpClient.Builder() 74 | .addInterceptor(withAccessTokenAndSecretInterceptor) 75 | //.addInterceptor(loggingInterceptor) // uncomment to debug network requests 76 | .build(); 77 | Retrofit withAccessTokenAndSecretRetrofit = 78 | sRetrofitBuilder.client(withAccessTokenAndSecretClient).build(); 79 | return withAccessTokenAndSecretRetrofit.create(TwitterAPI.class); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/inject/ApplicationComponent.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.inject; 2 | 3 | 4 | import org.loklak.wok.ui.suggestion.SuggestFragment; 5 | 6 | import dagger.Component; 7 | 8 | @Component(modules = ApplicationModule.class) 9 | public interface ApplicationComponent { 10 | 11 | 12 | void inject(SuggestFragment suggestFragment); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/inject/ApplicationModule.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.inject; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import org.loklak.wok.Utility; 6 | import org.loklak.wok.api.loklak.LoklakAPI; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | import io.realm.Realm; 11 | import retrofit2.Retrofit; 12 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 13 | import retrofit2.converter.gson.GsonConverterFactory; 14 | 15 | 16 | @Module 17 | public class ApplicationModule { 18 | 19 | private String mBaseUrl; 20 | 21 | public ApplicationModule(String baseUrl) { 22 | this.mBaseUrl = baseUrl; 23 | } 24 | 25 | @Provides 26 | Retrofit providesRetrofit() { 27 | Gson gson = Utility.getGsonForPrivateVariableClass(); 28 | return new Retrofit.Builder() 29 | .baseUrl(mBaseUrl) 30 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 31 | .addConverterFactory(GsonConverterFactory.create(gson)) 32 | .build(); 33 | } 34 | 35 | @Provides 36 | LoklakAPI providesLoklakAPI(Retrofit retrofit) { 37 | return retrofit.create(LoklakAPI.class); 38 | } 39 | 40 | @Provides 41 | Realm providesRealm() { 42 | return Realm.getDefaultInstance(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/harvest/Push.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.harvest; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class Push { 6 | 7 | private String mStatus; 8 | private int mRecords; 9 | @SerializedName("mps") 10 | private int mMessagesPerSecond; 11 | private String mMessage; 12 | 13 | public String getStatus() { 14 | return mStatus; 15 | } 16 | 17 | public int getRecords() { 18 | return mRecords; 19 | } 20 | 21 | public int getMps() { 22 | return mMessagesPerSecond; 23 | } 24 | 25 | public String getMessage() { 26 | return mMessage; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/harvest/ScrapedData.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.harvest; 2 | 3 | import java.util.List; 4 | 5 | public class ScrapedData { 6 | 7 | private List mStatuses; 8 | private String mQuery; 9 | 10 | public List getStatuses() { 11 | return mStatuses; 12 | } 13 | 14 | public void setStatuses(List statuses) { 15 | this.mStatuses = statuses; 16 | } 17 | 18 | public String getQuery() { 19 | return mQuery; 20 | } 21 | 22 | public void setQuery(String mQuery) { 23 | this.mQuery = mQuery; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/harvest/Status.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.harvest; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class Status implements Parcelable{ 10 | 11 | private User mUser; 12 | private String mScreenName; 13 | private String mLink; 14 | private long mCreatedAt; 15 | private long mTimestamp; 16 | private String mText; 17 | private String mIdStr; 18 | private Integer mRetweetCount; 19 | private Integer mFavouritesCount; 20 | private Double[] mlocationPoint; 21 | private List mImages; 22 | private List mVideos; 23 | 24 | public Status(User user, String screenName, String link, Long createdAt, 25 | Long timeStamp, String text, String id, int retweetCount) { 26 | this.mUser = user; 27 | this.mScreenName = screenName; 28 | this.mLink = link; 29 | this.mCreatedAt = createdAt; 30 | this.mTimestamp = timeStamp; 31 | this.mText = text; 32 | this.mIdStr = id; 33 | this.mRetweetCount = retweetCount; 34 | this.mFavouritesCount = 0; 35 | mImages = new ArrayList<>(); 36 | mVideos = new ArrayList<>(); 37 | } 38 | 39 | public User getUser() { 40 | return mUser; 41 | } 42 | 43 | public String getmScreenName() { 44 | return mScreenName; 45 | } 46 | 47 | public String getLink() { 48 | return mLink; 49 | } 50 | 51 | public long getCreatedAt() { 52 | return mCreatedAt; 53 | } 54 | 55 | public long getTimestamp() { 56 | return mTimestamp; 57 | } 58 | 59 | public String getText() { 60 | return mText; 61 | } 62 | 63 | public String getIdStr() { 64 | return mIdStr; 65 | } 66 | 67 | public Integer getRetweetCount() { 68 | return mRetweetCount; 69 | } 70 | 71 | public Integer getFavouritesCount() { 72 | return mFavouritesCount; 73 | } 74 | 75 | public List getImages() { 76 | return mImages; 77 | } 78 | 79 | public List getVideos() { 80 | return mVideos; 81 | } 82 | 83 | public void setUser(User mUser) { 84 | this.mUser = mUser; 85 | } 86 | 87 | public void setScreenName(String mScreenName) { 88 | this.mScreenName = mScreenName; 89 | } 90 | 91 | public void setLink(String mLink) { 92 | this.mLink = mLink; 93 | } 94 | 95 | public void setCreatedAt(long mCreatedAt) { 96 | this.mCreatedAt = mCreatedAt; 97 | } 98 | 99 | public void setTimestamp(long mTimestamp) { 100 | this.mTimestamp = mTimestamp; 101 | } 102 | 103 | public void setText(String mText) { 104 | this.mText = mText; 105 | } 106 | 107 | public void setIdStr(String mIdStr) { 108 | this.mIdStr = mIdStr; 109 | } 110 | 111 | public void setRetweetCount(Integer mRetweetCount) { 112 | this.mRetweetCount = mRetweetCount; 113 | } 114 | 115 | public void setFavouritesCount(Integer mFavouritesCount) { 116 | this.mFavouritesCount = mFavouritesCount; 117 | } 118 | 119 | public void setLocationPoint(Double latitude, Double longitude) { 120 | mlocationPoint = new Double[2]; 121 | mlocationPoint[0] = longitude; 122 | mlocationPoint[1] = latitude; 123 | } 124 | 125 | public void setImages(List mImages) { 126 | this.mImages = mImages; 127 | } 128 | 129 | public void setVideos(List mVideos) { 130 | this.mVideos = mVideos; 131 | } 132 | 133 | @Override 134 | public int describeContents() { 135 | return 0; 136 | } 137 | 138 | @Override 139 | public void writeToParcel(Parcel dest, int flags) { 140 | dest.writeParcelable(mUser, flags); 141 | dest.writeLong(mCreatedAt); 142 | dest.writeString(mText); 143 | } 144 | 145 | private Status(Parcel source) { 146 | mUser = source.readParcelable(User.class.getClassLoader()); 147 | mCreatedAt = source.readLong(); 148 | mText = source.readString(); 149 | } 150 | 151 | public static final Parcelable.Creator CREATOR = new Creator() { 152 | @Override 153 | public Status createFromParcel(Parcel source) { 154 | return new Status(source); 155 | } 156 | 157 | @Override 158 | public Status[] newArray(int size) { 159 | return new Status[size]; 160 | } 161 | }; 162 | } 163 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/harvest/User.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.harvest; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class User implements Parcelable { 7 | 8 | private String mName; 9 | private String mScreenName; 10 | private String mProfileImageUrlHttps; 11 | private String mUserId; 12 | private String mAppearanceFirst; 13 | private String mAppearanceLatest; 14 | 15 | public User(String name, String screenName, String profileImageUrlHttps, 16 | String userId, String appearanceFirst, String appearanceLatest) { 17 | this.mName = name; 18 | this.mScreenName = screenName; 19 | this.mProfileImageUrlHttps = profileImageUrlHttps; 20 | this.mUserId = userId; 21 | this.mAppearanceFirst = appearanceFirst; 22 | this.mAppearanceLatest = appearanceLatest; 23 | } 24 | 25 | public String getProfileImageUrlHttps() { 26 | return mProfileImageUrlHttps; 27 | } 28 | 29 | public String getName() { 30 | return mName; 31 | } 32 | 33 | public String getScreenName() { 34 | return mScreenName; 35 | } 36 | 37 | 38 | public void setAppearanceFirst(String mAppearanceFirst) { 39 | this.mAppearanceFirst = mAppearanceFirst; 40 | } 41 | 42 | public void setAppearanceLatest(String mAppearanceLatest) { 43 | this.mAppearanceLatest = mAppearanceLatest; 44 | } 45 | 46 | public void setUserId(String mUserId) { 47 | this.mUserId = mUserId; 48 | } 49 | 50 | public void setProfileImageUrlHttps(String mProfileImageUrlHttps) { 51 | this.mProfileImageUrlHttps = mProfileImageUrlHttps; 52 | } 53 | 54 | public void setName(String mName) { 55 | this.mName = mName; 56 | } 57 | 58 | public void setScreenName(String mScreenName) { 59 | this.mScreenName = mScreenName; 60 | } 61 | 62 | @Override 63 | public int describeContents() { 64 | return 0; 65 | } 66 | 67 | @Override 68 | public void writeToParcel(Parcel dest, int flags) { 69 | dest.writeString(mName); 70 | dest.writeString(mScreenName); 71 | } 72 | 73 | private User(Parcel parcel) { 74 | mName = parcel.readString(); 75 | mScreenName = parcel.readString(); 76 | } 77 | 78 | public static final Parcelable.Creator CREATOR = new Creator() { 79 | @Override 80 | public User createFromParcel(Parcel source) { 81 | return new User(source); 82 | } 83 | 84 | @Override 85 | public User[] newArray(int size) { 86 | return new User[size]; 87 | } 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/search/Search.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.search; 2 | 3 | import java.util.List; 4 | 5 | public class 6 | 7 | 8 | Search { 9 | 10 | private SearchMetadata mSearchMetadata; 11 | private List mStatuses = null; 12 | private List aggregations; 13 | 14 | public List getStatuses() { 15 | return mStatuses; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/search/SearchMetadata.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.search; 2 | 3 | 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | public class SearchMetadata { 7 | 8 | @SerializedName("startRecord") 9 | private String mStartRecord; 10 | @SerializedName("maximumRecords") 11 | private String mMaximumRecords; 12 | private String mCount; 13 | private Integer mHits; 14 | private Long mPeriod; 15 | private String mQuery; 16 | private String mFilter; 17 | private String mClient; 18 | private Integer mTime; 19 | private Integer mCountTwitterAll; 20 | private Integer mCountTwitterNew; 21 | private Integer mCountBackend; 22 | private Integer mCacheHits; 23 | private String mIndex; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/search/Status.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.search; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class Status implements Parcelable{ 10 | 11 | private String mTimestamp; 12 | private String mCreatedAt; 13 | private String mScreenName; 14 | private String mText; 15 | private String mLink; 16 | private String mIdStr; 17 | private Integer mRetweetCount; 18 | private Integer mFavouritesCount; 19 | private List mImages = new ArrayList<>(); 20 | private List mAudio = null; 21 | private List mVideos = null; 22 | private User mUser; 23 | 24 | public String getTimestamp() { 25 | return mTimestamp; 26 | } 27 | 28 | public String getCreatedAt() { 29 | return mCreatedAt; 30 | } 31 | 32 | public String getText() { 33 | return mText; 34 | } 35 | 36 | public String getLink() { 37 | return mLink; 38 | } 39 | 40 | public String getIdStr() { 41 | return mIdStr; 42 | } 43 | 44 | public Integer getRetweetCount() { 45 | return mRetweetCount; 46 | } 47 | 48 | public Integer getFavouritesCount() { 49 | return mFavouritesCount; 50 | } 51 | 52 | public List getImages() { 53 | return mImages; 54 | } 55 | 56 | public List getAudio() { 57 | return mAudio; 58 | } 59 | 60 | public List getVideos() { 61 | return mVideos; 62 | } 63 | 64 | public User getUser() { 65 | return mUser; 66 | } 67 | 68 | @Override 69 | public int describeContents() { 70 | return 0; 71 | } 72 | 73 | @Override 74 | public void writeToParcel(Parcel dest, int flags) { 75 | dest.writeString(mText); 76 | dest.writeInt(mRetweetCount); 77 | dest.writeInt(mFavouritesCount); 78 | dest.writeStringList(mImages); 79 | dest.writeParcelable(mUser, flags); 80 | } 81 | 82 | private Status(Parcel source) { 83 | mText = source.readString(); 84 | mRetweetCount = source.readInt(); 85 | mFavouritesCount = source.readInt(); 86 | mImages = source.createStringArrayList(); 87 | mUser = source.readParcelable(User.class.getClassLoader()); 88 | } 89 | 90 | public static final Parcelable.Creator CREATOR = new Creator() { 91 | @Override 92 | public Status createFromParcel(Parcel source) { 93 | return new Status(source); 94 | } 95 | 96 | @Override 97 | public Status[] newArray(int size) { 98 | return new Status[size]; 99 | } 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/search/User.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.search; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class User implements Parcelable { 7 | 8 | private String mProfileImageUrlHttps; 9 | private String mScreenName; 10 | private String mUserId; 11 | private String mName; 12 | 13 | public String getProfileImageUrlHttps() { 14 | return mProfileImageUrlHttps; 15 | } 16 | 17 | public String getScreenName() { 18 | return mScreenName; 19 | } 20 | 21 | public String getUserId() { 22 | return mUserId; 23 | } 24 | 25 | public String getName() { 26 | return mName; 27 | } 28 | 29 | @Override 30 | public int describeContents() { 31 | return 0; 32 | } 33 | 34 | @Override 35 | public void writeToParcel(Parcel dest, int flags) { 36 | dest.writeString(mProfileImageUrlHttps); 37 | dest.writeString(mName); 38 | dest.writeString(mScreenName); 39 | } 40 | 41 | private User(Parcel parcel) { 42 | mProfileImageUrlHttps = parcel.readString(); 43 | mName = parcel.readString(); 44 | mScreenName = parcel.readString(); 45 | } 46 | 47 | public static final Parcelable.Creator CREATOR = new Creator() { 48 | @Override 49 | public User createFromParcel(Parcel source) { 50 | return new User(source); 51 | } 52 | 53 | @Override 54 | public User[] newArray(int size) { 55 | return new User[size]; 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/suggest/Query.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.suggest; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import io.realm.RealmObject; 6 | 7 | 8 | public class Query extends RealmObject { 9 | 10 | private String mQuery; 11 | private int mQueryCount; 12 | private int mMessagePeriod; 13 | private String mSourceType; 14 | private String mRetrievalLast; 15 | private int mMessagesPerDay; 16 | private int mQueryLength; 17 | @SerializedName("timezoneOffset") 18 | private int mTimezoneOffset; 19 | private String mRetrievalNext; 20 | private int mScoreRetrieval; 21 | private String mQueryLast; 22 | private String mExpectedNext; 23 | private int mScoreSuggest; 24 | private int mRetrievalCount; 25 | private String mQueryFirst; 26 | 27 | public Query() { 28 | mQuery = ""; 29 | mSourceType = ""; 30 | mRetrievalLast = ""; 31 | mRetrievalNext = ""; 32 | mQueryLast = ""; 33 | mExpectedNext = ""; 34 | mQueryFirst = ""; 35 | } 36 | 37 | public void setQuery(String query) { 38 | this.mQuery = query; 39 | } 40 | 41 | public String getQuery() { 42 | return mQuery; 43 | } 44 | 45 | public int getQueryCount() { 46 | return mQueryCount; 47 | } 48 | 49 | public int getMessagePeriod() { 50 | return mMessagePeriod; 51 | } 52 | 53 | public String getSourceType() { 54 | return mSourceType; 55 | } 56 | 57 | public String getRetrievalLast() { 58 | return mRetrievalLast; 59 | } 60 | 61 | public int getMessagesPerDay() { 62 | return mMessagesPerDay; 63 | } 64 | 65 | public int getQueryLength() { 66 | return mQueryLength; 67 | } 68 | 69 | public int getTimezoneOffset() { 70 | return mTimezoneOffset; 71 | } 72 | 73 | public String getRetrievalNext() { 74 | return mRetrievalNext; 75 | } 76 | 77 | public int getScoreRetrieval() { 78 | return mScoreRetrieval; 79 | } 80 | 81 | public String getQueryLast() { 82 | return mQueryLast; 83 | } 84 | 85 | public String getExpectedNext() { 86 | return mExpectedNext; 87 | } 88 | 89 | public int getScoreSuggest() { 90 | return mScoreSuggest; 91 | } 92 | 93 | public int getRetrievalCount() { 94 | return mRetrievalCount; 95 | } 96 | 97 | public String getQueryFirst() { 98 | return mQueryFirst; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/suggest/SearchMetadata.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.suggest; 2 | 3 | public class SearchMetadata { 4 | 5 | private String mCount; 6 | private Integer mHits; 7 | private String mQuery; 8 | private String mOrder; 9 | private String mOrderby; 10 | private String mClient; 11 | 12 | public String getCount() { 13 | return mCount; 14 | } 15 | 16 | public Integer getHits() { 17 | return mHits; 18 | } 19 | 20 | public String getQuery() { 21 | return mQuery; 22 | } 23 | 24 | public String getOrder() { 25 | return mOrder; 26 | } 27 | 28 | public String getOrderby() { 29 | return mOrderby; 30 | } 31 | 32 | public String getClient() { 33 | return mClient; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/suggest/SuggestData.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.suggest; 2 | 3 | 4 | import java.util.List; 5 | 6 | public class SuggestData { 7 | 8 | private SearchMetadata mSearchMetadata; 9 | private List mQueries; 10 | 11 | public void setQueries(List queries) { 12 | this.mQueries = queries; 13 | } 14 | 15 | public SearchMetadata getSearchMetadata() { 16 | return mSearchMetadata; 17 | } 18 | 19 | public List getQueries() { 20 | return mQueries; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/twitter/AccountVerifyCredentials.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.twitter; 2 | 3 | 4 | public class AccountVerifyCredentials { 5 | 6 | private String mCreatedAt; 7 | private String mIdStr; 8 | private String mProfileImageUrlHttps; 9 | private String mName; 10 | private String mScreenName; 11 | 12 | public String getCreatedAt() { 13 | return mCreatedAt; 14 | } 15 | 16 | public String getIdStr() { 17 | return mIdStr; 18 | } 19 | 20 | public String getProfileImageUrlHttps() { 21 | return mProfileImageUrlHttps; 22 | } 23 | 24 | public String getName() { 25 | return mName; 26 | } 27 | 28 | public String getScreenName() { 29 | return mScreenName; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/twitter/MediaEntity.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.twitter; 2 | 3 | 4 | public class MediaEntity { 5 | 6 | private long mId; 7 | private String mIdStr; 8 | private String mMediaUrl; 9 | private String mMediaUrlHttps; 10 | 11 | public long getId() { 12 | return mId; 13 | } 14 | 15 | public String getIdStr() { 16 | return mIdStr; 17 | } 18 | 19 | public String getMediaUrl() { 20 | return mMediaUrl; 21 | } 22 | 23 | public String getMediaUrlHttps() { 24 | return mMediaUrlHttps; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/twitter/MediaUpload.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.twitter; 2 | 3 | 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | public class MediaUpload { 7 | 8 | @SerializedName("media_id_string") 9 | private String mMediaIdString; 10 | private int mSize; 11 | private int mExpiresAfterSecs; 12 | 13 | public String getMediaIdString() { 14 | return mMediaIdString; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/twitter/StatusEntities.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.twitter; 2 | 3 | 4 | import java.util.List; 5 | 6 | public class StatusEntities { 7 | 8 | private List mMedia; 9 | 10 | public List getMediaList() { 11 | return mMedia; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/model/twitter/StatusUpdate.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.model.twitter; 2 | 3 | 4 | public class StatusUpdate { 5 | 6 | private String mIdStr; 7 | private String mText; 8 | private StatusEntities mExtendedEntities; 9 | private int mRetweetCount; 10 | 11 | public StatusUpdate(String idStr, String text, int retweetCount) { 12 | this.mIdStr = idStr; 13 | this.mText = text; 14 | this.mRetweetCount = retweetCount; 15 | } 16 | 17 | public String getIdStr() { 18 | return mIdStr; 19 | } 20 | 21 | public String getText() { 22 | return mText; 23 | } 24 | 25 | public StatusEntities getExtendedEntities() { 26 | return mExtendedEntities; 27 | } 28 | 29 | public int getRetweetCount() { 30 | return mRetweetCount; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/tools/LogLines.java: -------------------------------------------------------------------------------- 1 | /** 2 | * LogLines 3 | * Copyright 13.01.2016 by Michael Peter Christen, @0rb1t3r 4 | *

5 | * This library is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 2.1 of the License, or (at your option) any later version. 9 | *

10 | * This library is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | *

15 | * You should have received a copy of the GNU Lesser General Public License 16 | * along with this program in the file lgpl21.txt 17 | * If not, see . 18 | */ 19 | 20 | 21 | package org.loklak.wok.tools; 22 | 23 | import java.util.Iterator; 24 | import java.util.concurrent.BlockingQueue; 25 | import java.util.concurrent.LinkedBlockingQueue; 26 | 27 | /** 28 | * Implements a concurrent limited log line queue. 29 | * This shall be used to avoid memory leaks when the queue may 30 | * get too large and information from the queue can be abandoned safely. 31 | */ 32 | public class LogLines implements Iterable { 33 | 34 | public final BlockingQueue lines = new LinkedBlockingQueue<>(); 35 | 36 | private final int limit; 37 | 38 | public LogLines(int limit) { 39 | this.limit = limit; 40 | } 41 | 42 | public int size() { 43 | return lines.size(); 44 | } 45 | 46 | public void add(A line) { 47 | this.lines.add(line); 48 | while (this.size() > this.limit) this.lines.poll(); 49 | } 50 | 51 | public A poll() { 52 | return lines.poll(); 53 | } 54 | 55 | public Iterator iterator() { 56 | return this.lines.iterator(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/activity/SearchActivity.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.activity; 2 | 3 | import android.content.Intent; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | 7 | import org.loklak.wok.ui.fragment.SearchFragment; 8 | import org.loklak.wok.R; 9 | 10 | public class SearchActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | Intent intent = getIntent(); 16 | 17 | setContentView(R.layout.activity_search); 18 | if (savedInstanceState == null) { 19 | SearchFragment searchFragment = new SearchFragment(); 20 | getSupportFragmentManager() 21 | .beginTransaction() 22 | .replace(R.id.fragment_container, searchFragment) 23 | .commit(); 24 | } 25 | } 26 | 27 | @Override 28 | public void onBackPressed() { 29 | super.onBackPressed(); 30 | overridePendingTransition(R.anim.back_button_enter, R.anim.back_button_exit); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/activity/SplashActivity.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.activity; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.os.Handler; 6 | import android.support.v7.app.AppCompatActivity; 7 | 8 | import org.loklak.wok.R; 9 | 10 | 11 | public class SplashActivity extends AppCompatActivity { 12 | 13 | private Handler mHandler; 14 | 15 | private Runnable mRunnable = () -> { 16 | Intent intent = new Intent(this, TweetHarvestingActivity.class); 17 | startActivity(intent); 18 | finish(); 19 | }; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_splash); 25 | mHandler = new Handler(); 26 | } 27 | 28 | @Override 29 | protected void onResume() { 30 | super.onResume(); 31 | mHandler.postDelayed(mRunnable, 3000); 32 | } 33 | 34 | @Override 35 | public void onBackPressed() { 36 | super.onBackPressed(); 37 | removeCallback(); 38 | } 39 | 40 | @Override 41 | protected void onStop() { 42 | super.onStop(); 43 | removeCallback(); 44 | } 45 | 46 | private void removeCallback() { 47 | if (mHandler != null) { 48 | mHandler.removeCallbacks(mRunnable); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/activity/TweetHarvestingActivity.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.WindowManager; 7 | import android.widget.Toast; 8 | 9 | import org.loklak.wok.ui.fragment.TweetHarvestingFragment; 10 | import org.loklak.wok.R; 11 | 12 | public class TweetHarvestingActivity extends AppCompatActivity { 13 | 14 | private static long BackPressed; 15 | 16 | @Override 17 | protected void onCreate(@Nullable Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_tweet_harvesting); 20 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 21 | if (savedInstanceState == null) { 22 | TweetHarvestingFragment tweetHarvestingFragment = new TweetHarvestingFragment(); 23 | getSupportFragmentManager() 24 | .beginTransaction() 25 | .replace(R.id.fragment_container, tweetHarvestingFragment) 26 | .commit(); 27 | } 28 | } 29 | 30 | @Override 31 | public void onBackPressed() { 32 | if (BackPressed+2000>System.currentTimeMillis()) { 33 | 34 | super.onBackPressed(); 35 | } 36 | else{ 37 | Toast.makeText(getBaseContext(),"Press once again to exit",Toast.LENGTH_SHORT).show(); 38 | } 39 | BackPressed = System.currentTimeMillis(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/activity/TweetPostingActivity.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.activity; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | 6 | import org.loklak.wok.ui.fragment.TweetPostingFragment; 7 | import org.loklak.wok.R; 8 | 9 | 10 | public class TweetPostingActivity extends AppCompatActivity { 11 | 12 | private final String LOG_TAG = TweetPostingActivity.class.getName(); 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_tweet_posting); 18 | if (savedInstanceState == null) { 19 | TweetPostingFragment tweetPostingFragment = new TweetPostingFragment(); 20 | getSupportFragmentManager() 21 | .beginTransaction() 22 | .replace(R.id.fragment_container, tweetPostingFragment) 23 | .commit(); 24 | } 25 | } 26 | 27 | @Override 28 | public void onBackPressed() { 29 | super.onBackPressed(); 30 | overridePendingTransition(R.anim.back_button_top_enter, R.anim.back_button_bottom_exit); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/fragment/SearchCategoryFragment.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.fragment; 2 | 3 | 4 | import android.content.res.Resources; 5 | import android.os.Bundle; 6 | import android.os.Parcelable; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.app.Fragment; 9 | import android.support.v7.widget.LinearLayoutManager; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.util.Log; 12 | import android.view.LayoutInflater; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.ProgressBar; 16 | import android.widget.TextView; 17 | 18 | import org.loklak.wok.adapters.SearchCategoryAdapter; 19 | import org.loklak.wok.api.loklak.LoklakAPI; 20 | import org.loklak.wok.api.loklak.RestClient; 21 | import org.loklak.wok.model.search.Search; 22 | import org.loklak.wok.model.search.Status; 23 | import org.loklak.wok.utility.Constants; 24 | import org.loklak.wok.R; 25 | 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | import butterknife.BindView; 30 | import butterknife.ButterKnife; 31 | import butterknife.OnClick; 32 | import io.reactivex.android.schedulers.AndroidSchedulers; 33 | import io.reactivex.schedulers.Schedulers; 34 | 35 | public class SearchCategoryFragment extends Fragment { 36 | 37 | private static final String TWEET_SEARCH_CATEGORY_KEY = "tweet-search-category"; 38 | private final String LOG_TAG = SearchCategoryFragment.class.getName(); 39 | private final String PARCELABLE_SEARCHED_TWEETS = "searched_tweets"; 40 | private final String PARCELABLE_RECYCLER_VIEW_STATE = "recycler_view_state"; 41 | 42 | @BindView(R.id.searched_tweet_container) 43 | RecyclerView recyclerView; 44 | @BindView(R.id.progress_bar) 45 | ProgressBar progressBar; 46 | @BindView(R.id.no_search_result_found) 47 | TextView noSearchResultFoundTextView; 48 | @BindView(R.id.network_error) 49 | TextView networkErrorTextView; 50 | 51 | private SearchCategoryAdapter mSearchCategoryAdapter; 52 | 53 | private String mSearchQuery; 54 | private String mTweetSearchCategory; 55 | 56 | public SearchCategoryFragment() { 57 | // Required empty public constructor 58 | } 59 | 60 | public static SearchCategoryFragment newInstance(String category, String query) { 61 | Bundle args = new Bundle(); 62 | args.putString(Constants.TWEET_SEARCH_SUGGESTION_QUERY_KEY, query); 63 | args.putString(TWEET_SEARCH_CATEGORY_KEY, category); 64 | SearchCategoryFragment fragment = new SearchCategoryFragment(); 65 | fragment.setArguments(args); 66 | return fragment; 67 | } 68 | 69 | @Override 70 | public void onCreate(@Nullable Bundle savedInstanceState) { 71 | super.onCreate(savedInstanceState); 72 | Bundle bundle = getArguments(); 73 | if (bundle != null) { 74 | mTweetSearchCategory = bundle.getString(TWEET_SEARCH_CATEGORY_KEY); 75 | mSearchQuery = bundle.getString(Constants.TWEET_SEARCH_SUGGESTION_QUERY_KEY); 76 | } 77 | } 78 | 79 | @Override 80 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 81 | Bundle savedInstanceState) { 82 | // Inflate the layout for this fragment 83 | View view = inflater.inflate(R.layout.fragment_search_category, container, false); 84 | ButterKnife.bind(this, view); 85 | 86 | mSearchCategoryAdapter = new SearchCategoryAdapter(getActivity(), new ArrayList<>()); 87 | recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); 88 | if (savedInstanceState != null) { 89 | Parcelable recyclerViewState = 90 | savedInstanceState.getParcelable(PARCELABLE_RECYCLER_VIEW_STATE); 91 | recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState); 92 | 93 | List searchedTweets = 94 | savedInstanceState.getParcelableArrayList(PARCELABLE_SEARCHED_TWEETS); 95 | mSearchCategoryAdapter.setStatuses(searchedTweets); 96 | progressBar.setVisibility(View.GONE); 97 | recyclerView.setVisibility(View.VISIBLE); 98 | } else { 99 | fetchSearchedTweets(); 100 | } 101 | recyclerView.setAdapter(mSearchCategoryAdapter); 102 | 103 | return view; 104 | } 105 | 106 | @Override 107 | public void onSaveInstanceState(Bundle outState) { 108 | super.onSaveInstanceState(outState); 109 | ArrayList searchedTweets = mSearchCategoryAdapter.getStatuses(); 110 | outState.putParcelableArrayList(PARCELABLE_SEARCHED_TWEETS, searchedTweets); 111 | 112 | Parcelable recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState(); 113 | outState.putParcelable(PARCELABLE_RECYCLER_VIEW_STATE, recyclerViewState); 114 | } 115 | 116 | private void fetchSearchedTweets() { 117 | progressBar.setVisibility(View.VISIBLE); 118 | LoklakAPI loklakAPI = RestClient.createApi(LoklakAPI.class); 119 | loklakAPI.getSearchedTweets(mSearchQuery, mTweetSearchCategory, 30) 120 | .subscribeOn(Schedulers.io()) 121 | .observeOn(AndroidSchedulers.mainThread()) 122 | .subscribe(this::setSearchResultView, this::setNetworkErrorView); 123 | } 124 | 125 | private void setSearchResultView(Search search) { 126 | List statusList = search.getStatuses(); 127 | networkErrorTextView.setVisibility(View.GONE); 128 | progressBar.setVisibility(View.GONE); 129 | if (statusList.size() == 0) { 130 | recyclerView.setVisibility(View.GONE); 131 | 132 | Resources res = getResources(); 133 | String noSearchResultMessage = res.getString(R.string.no_search_match, mSearchQuery); 134 | noSearchResultFoundTextView.setVisibility(View.VISIBLE); 135 | noSearchResultFoundTextView.setText(noSearchResultMessage); 136 | } else { 137 | recyclerView.setVisibility(View.VISIBLE); 138 | mSearchCategoryAdapter.setStatuses(statusList); 139 | } 140 | } 141 | 142 | private void setNetworkErrorView(Throwable throwable) { 143 | Log.e(LOG_TAG, throwable.toString()); 144 | progressBar.setVisibility(View.GONE); 145 | recyclerView.setVisibility(View.GONE); 146 | networkErrorTextView.setVisibility(View.VISIBLE); 147 | } 148 | 149 | @OnClick(R.id.network_error) 150 | public void setOnClickNetworkErrorTextViewListener() { 151 | networkErrorTextView.setVisibility(View.GONE); 152 | fetchSearchedTweets(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/fragment/SearchFragment.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.fragment; 2 | 3 | 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.design.widget.TabLayout; 7 | import android.support.v4.app.Fragment; 8 | import android.support.v4.app.FragmentManager; 9 | import android.support.v4.view.ViewPager; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.support.v7.widget.Toolbar; 12 | import android.view.LayoutInflater; 13 | import android.view.View; 14 | import android.view.ViewGroup; 15 | import android.widget.EditText; 16 | import android.widget.ImageButton; 17 | 18 | import org.loklak.wok.adapters.SearchFragmentPagerAdapter; 19 | import org.loklak.wok.ui.suggestion.SuggestActivity; 20 | import org.loklak.wok.ui.activity.TweetPostingActivity; 21 | import org.loklak.wok.utility.Constants; 22 | import org.loklak.wok.R; 23 | 24 | import butterknife.BindView; 25 | import butterknife.ButterKnife; 26 | import butterknife.OnClick; 27 | 28 | 29 | public class SearchFragment extends Fragment { 30 | 31 | private final String LOG_TAG = SearchFragment.class.getName(); 32 | 33 | @BindView(R.id.toolbar) 34 | Toolbar toolbar; 35 | @BindView(R.id.tweet_search_edit_text) 36 | EditText tweetSearchEditText; 37 | @BindView(R.id.clear_image_button) 38 | ImageButton clearImageButton; 39 | @BindView(R.id.tabs) 40 | TabLayout tabLayout; 41 | @BindView(R.id.view_pager) 42 | ViewPager viewPager; 43 | 44 | private String mQuery; 45 | 46 | public SearchFragment() { 47 | // Required empty public constructor 48 | } 49 | 50 | @Override 51 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 52 | Bundle savedInstanceState) { 53 | // Inflate the layout for this fragment 54 | View rootView = inflater.inflate(R.layout.fragment_search, container, false); 55 | ButterKnife.bind(this, rootView); 56 | 57 | AppCompatActivity appCompatActivity = (AppCompatActivity) getActivity(); 58 | appCompatActivity.setSupportActionBar(toolbar); 59 | toolbar.setNavigationIcon(R.drawable.ic_arrow_back_24dp); 60 | toolbar.setNavigationOnClickListener(view -> getActivity().onBackPressed()); 61 | 62 | Intent intent = getActivity().getIntent(); 63 | mQuery = intent.getStringExtra(Constants.TWEET_SEARCH_SUGGESTION_QUERY_KEY); 64 | tweetSearchEditText.setOnFocusChangeListener((view, hasFocus) -> { 65 | if (hasFocus) { 66 | Intent suggestIntent = new Intent(getActivity(), SuggestActivity.class); 67 | startActivity(suggestIntent); 68 | getActivity().overridePendingTransition(R.anim.back_button_enter, R.anim.back_button_exit); 69 | } 70 | }); 71 | 72 | tabLayout.setupWithViewPager(viewPager); 73 | setupViewPager(viewPager); 74 | 75 | return rootView; 76 | } 77 | 78 | @Override 79 | public void onStart() { 80 | super.onStart(); 81 | tweetSearchEditText.setText(mQuery); 82 | } 83 | 84 | @OnClick(R.id.tweet_post) 85 | public void onClickFab() { 86 | Intent intent = new Intent(getActivity(), TweetPostingActivity.class); 87 | startActivity(intent); 88 | getActivity().overridePendingTransition(R.anim.bottom_enter, R.anim.top_exit); 89 | } 90 | 91 | private void setupViewPager(ViewPager viewPager) { 92 | FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); 93 | SearchFragmentPagerAdapter pagerAdapter = new SearchFragmentPagerAdapter(fragmentManager); 94 | pagerAdapter.addFragment(SearchCategoryFragment.newInstance("", mQuery), "LATEST"); 95 | pagerAdapter.addFragment(SearchCategoryFragment.newInstance("image", mQuery), "PHOTOS"); 96 | pagerAdapter.addFragment(SearchCategoryFragment.newInstance("video", mQuery), "VIDEOS"); 97 | viewPager.setAdapter(pagerAdapter); 98 | } 99 | 100 | @OnClick(R.id.clear_image_button) 101 | public void setOnClickClearImageButtonListener() { 102 | tweetSearchEditText.setText(""); 103 | tweetSearchEditText.clearFocus(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/suggestion/SuggestActivity.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.suggestion; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | 6 | import org.loklak.wok.R; 7 | 8 | public class SuggestActivity extends AppCompatActivity { 9 | 10 | @Override 11 | protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | setContentView(R.layout.activity_suggest); 14 | if (savedInstanceState == null) { 15 | SuggestFragment suggestFragment = new SuggestFragment(); 16 | getSupportFragmentManager() 17 | .beginTransaction() 18 | .replace(R.id.fragment_container, suggestFragment) 19 | .commit(); 20 | } 21 | } 22 | 23 | @Override 24 | public void onBackPressed() { 25 | super.onBackPressed(); 26 | overridePendingTransition(R.anim.back_button_enter, R.anim.back_button_exit); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/suggestion/SuggestContract.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.suggestion; 2 | 3 | import org.loklak.wok.model.suggest.Query; 4 | 5 | import java.util.List; 6 | 7 | import io.reactivex.Observable; 8 | 9 | 10 | public interface SuggestContract { 11 | 12 | interface View { 13 | 14 | void showProgressBar(boolean show); 15 | 16 | void onSuggestionFetchSuccessful(List queries); 17 | 18 | void onSuggestionFetchError(Throwable throwable); 19 | } 20 | 21 | interface Presenter { 22 | 23 | void attachView(View view); 24 | 25 | void createCompositeDisposable(); 26 | 27 | void loadSuggestionsFromAPI(String query, boolean showProgressBar); 28 | 29 | void loadSuggestionsFromDatabase(); 30 | 31 | void saveSuggestions(List queries); 32 | 33 | void suggestionQueryChanged(Observable observable); 34 | 35 | void detachView(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/suggestion/SuggestFragment.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.suggestion; 2 | 3 | 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.os.Parcelable; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.app.Fragment; 9 | import android.support.v4.widget.SwipeRefreshLayout; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.support.v7.widget.LinearLayoutManager; 12 | import android.support.v7.widget.RecyclerView; 13 | import android.support.v7.widget.Toolbar; 14 | import android.util.Log; 15 | import android.view.LayoutInflater; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.view.inputmethod.EditorInfo; 19 | import android.widget.EditText; 20 | import android.widget.ImageButton; 21 | import android.widget.Toast; 22 | 23 | import com.jakewharton.rxbinding2.widget.RxTextView; 24 | 25 | import org.loklak.wok.Constants; 26 | import org.loklak.wok.LoklakWokApplication; 27 | import org.loklak.wok.R; 28 | import org.loklak.wok.Utility; 29 | import org.loklak.wok.adapters.SuggestAdapter; 30 | import org.loklak.wok.model.suggest.Query; 31 | import org.loklak.wok.ui.activity.SearchActivity; 32 | import org.loklak.wok.ui.activity.TweetPostingActivity; 33 | 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | import java.util.concurrent.TimeUnit; 37 | 38 | import javax.inject.Inject; 39 | 40 | import butterknife.BindString; 41 | import butterknife.BindView; 42 | import butterknife.ButterKnife; 43 | import butterknife.OnClick; 44 | import io.reactivex.Observable; 45 | 46 | 47 | public class SuggestFragment extends Fragment 48 | implements SuggestAdapter.OnSuggestionClickListener, SuggestContract.View{ 49 | 50 | private final String PARCELABLE_RECYCLER_VIEW_STATE = "recycler_view_state"; 51 | 52 | @BindView(R.id.toolbar) 53 | Toolbar toolbar; 54 | 55 | @BindView(R.id.tweet_search_edit_text) 56 | EditText tweetSearchEditText; 57 | 58 | @BindView(R.id.clear_image_button) 59 | ImageButton clearImageButton; 60 | 61 | @BindView(R.id.refresh_suggestions) 62 | SwipeRefreshLayout refreshSuggestions; 63 | 64 | @BindView(R.id.tweet_search_suggestions) 65 | RecyclerView tweetSearchSuggestions; 66 | 67 | @BindString(R.string.network_request_error) 68 | String networkRequestError; 69 | 70 | @Inject 71 | SuggestPresenter suggestPresenter; 72 | 73 | private Toast mToast; 74 | 75 | private SuggestAdapter mSuggestAdapter; 76 | 77 | private final String LOG_TAG = SuggestFragment.class.getName(); 78 | 79 | @Override 80 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 81 | Bundle savedInstanceState) { 82 | // Inflate the layout for this fragment 83 | View rootView = inflater.inflate(R.layout.fragment_suggest, container, false); 84 | ButterKnife.bind(this, rootView); 85 | 86 | LoklakWokApplication application = (LoklakWokApplication) getActivity().getApplication(); 87 | application.getApplicationComponent().inject(this); 88 | suggestPresenter.attachView(this); 89 | 90 | return rootView; 91 | } 92 | 93 | @Override 94 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 95 | AppCompatActivity activity = (AppCompatActivity) getActivity(); 96 | activity.setSupportActionBar(toolbar); 97 | toolbar.setNavigationIcon(R.drawable.ic_arrow_back_24dp); 98 | toolbar.setNavigationOnClickListener(v -> getActivity().onBackPressed()); 99 | 100 | tweetSearchEditText.setOnEditorActionListener((textView, actionId, event) -> { 101 | if (actionId == EditorInfo.IME_ACTION_SEARCH) { 102 | String searchQuery = tweetSearchEditText.getText().toString(); 103 | startSearchActivity(searchQuery); 104 | return true; 105 | } 106 | return false; 107 | }); 108 | 109 | refreshSuggestions.setOnRefreshListener(() -> { 110 | String query = tweetSearchEditText.getText().toString(); 111 | suggestPresenter.loadSuggestionsFromAPI(query, true); 112 | }); 113 | 114 | mSuggestAdapter = new SuggestAdapter(new ArrayList<>(), this); 115 | tweetSearchSuggestions.setLayoutManager(new LinearLayoutManager(getActivity())); 116 | tweetSearchSuggestions.setAdapter(mSuggestAdapter); 117 | 118 | suggestPresenter.loadSuggestionsFromDatabase(); 119 | 120 | if (savedInstanceState != null) { 121 | Parcelable recyclerViewState = 122 | savedInstanceState.getParcelable(PARCELABLE_RECYCLER_VIEW_STATE); 123 | tweetSearchSuggestions.getLayoutManager().onRestoreInstanceState(recyclerViewState); 124 | } else { 125 | suggestPresenter.loadSuggestionsFromAPI("", true); 126 | } 127 | } 128 | 129 | @Override 130 | public void onStart() { 131 | super.onStart(); 132 | suggestPresenter.createCompositeDisposable(); 133 | Observable observable 134 | = RxTextView.textChanges(tweetSearchEditText).debounce(400, TimeUnit.MILLISECONDS); 135 | suggestPresenter.suggestionQueryChanged(observable); 136 | } 137 | 138 | private void startSearchActivity(String searchQuery) { 139 | Intent intent = new Intent(getActivity(), SearchActivity.class); 140 | intent.putExtra(Constants.TWEET_SEARCH_SUGGESTION_QUERY_KEY, searchQuery); 141 | startActivity(intent); 142 | getActivity().overridePendingTransition(R.anim.enter, R.anim.exit); 143 | } 144 | 145 | @OnClick(R.id.clear_image_button) 146 | public void onClickedClearImageButton() { 147 | tweetSearchEditText.setText(""); 148 | } 149 | 150 | @Override 151 | public void onSaveInstanceState(Bundle outState) { 152 | super.onSaveInstanceState(outState); 153 | Parcelable recyclerViewState = 154 | tweetSearchSuggestions.getLayoutManager().onSaveInstanceState(); 155 | outState.putParcelable(PARCELABLE_RECYCLER_VIEW_STATE, recyclerViewState); 156 | } 157 | 158 | @Override 159 | public void onStop() { 160 | super.onStop(); 161 | List queries = mSuggestAdapter.getQueries(); 162 | suggestPresenter.saveSuggestions(queries); 163 | suggestPresenter.detachView(); 164 | } 165 | 166 | @Override 167 | public void onSuggestionClicked(Query query) { 168 | String searchQuery = query.getQuery(); 169 | startSearchActivity(searchQuery); 170 | } 171 | 172 | @OnClick(R.id.tweet_post) 173 | public void onClickFab() { 174 | Intent intent = new Intent(getActivity(), TweetPostingActivity.class); 175 | startActivity(intent); 176 | getActivity().overridePendingTransition(R.anim.bottom_enter, R.anim.top_exit); 177 | } 178 | 179 | @Override 180 | public void onSuggestionFetchSuccessful(List queries) { 181 | if (queries != null) { 182 | mSuggestAdapter.setQueries(queries); 183 | } 184 | } 185 | 186 | @Override 187 | public void showProgressBar(boolean show) { 188 | if (show) { 189 | refreshSuggestions.setRefreshing(true); 190 | tweetSearchSuggestions.setVisibility(View.INVISIBLE); 191 | } else { 192 | refreshSuggestions.setRefreshing(false); 193 | tweetSearchSuggestions.setVisibility(View.VISIBLE); 194 | } 195 | } 196 | 197 | @Override 198 | public void onSuggestionFetchError(Throwable throwable) { 199 | Log.e(LOG_TAG, throwable.toString()); 200 | Utility.displayToast(mToast, getActivity(), networkRequestError); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/ui/suggestion/SuggestPresenter.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.ui.suggestion; 2 | 3 | 4 | import org.loklak.wok.api.loklak.LoklakAPI; 5 | import org.loklak.wok.model.suggest.Query; 6 | 7 | import java.util.List; 8 | 9 | import javax.inject.Inject; 10 | 11 | import io.reactivex.Observable; 12 | import io.reactivex.android.schedulers.AndroidSchedulers; 13 | import io.reactivex.disposables.CompositeDisposable; 14 | import io.reactivex.schedulers.Schedulers; 15 | import io.realm.Realm; 16 | import io.realm.RealmResults; 17 | 18 | public class SuggestPresenter implements SuggestContract.Presenter { 19 | 20 | private final Realm mRealm; 21 | private LoklakAPI mLoklakAPI; 22 | private SuggestContract.View mView; 23 | private CompositeDisposable mCompositeDisposable; 24 | 25 | @Inject 26 | public SuggestPresenter(LoklakAPI loklakAPI, Realm realm) { 27 | this.mLoklakAPI = loklakAPI; 28 | this.mRealm = realm; 29 | mCompositeDisposable = new CompositeDisposable(); 30 | } 31 | 32 | @Override 33 | public void attachView(SuggestContract.View view) { 34 | this.mView = view; 35 | } 36 | 37 | @Override 38 | public void createCompositeDisposable() { 39 | if (mCompositeDisposable == null ) { 40 | mCompositeDisposable = new CompositeDisposable(); 41 | } 42 | } 43 | 44 | @Override 45 | public void loadSuggestionsFromAPI(String query, boolean showProgressBar) { 46 | mView.showProgressBar(showProgressBar); 47 | mCompositeDisposable.add(mLoklakAPI.getSuggestions(query) 48 | .flatMap(suggestData -> Observable.just(suggestData.getQueries())) 49 | .subscribeOn(Schedulers.io()) 50 | .observeOn(AndroidSchedulers.mainThread()) 51 | .subscribe( 52 | mView::onSuggestionFetchSuccessful, 53 | throwable -> { 54 | mView.showProgressBar(false); 55 | mView.onSuggestionFetchError(throwable); 56 | }, 57 | () -> mView.showProgressBar(false) 58 | ) 59 | ); 60 | } 61 | 62 | @Override 63 | public void loadSuggestionsFromDatabase() { 64 | RealmResults realmResults = mRealm.where(Query.class).findAll(); 65 | List queries = mRealm.copyFromRealm(realmResults); 66 | mView.onSuggestionFetchSuccessful(queries); 67 | } 68 | 69 | @Override 70 | public void saveSuggestions(List queries) { 71 | mRealm.beginTransaction(); 72 | mRealm.where(Query.class).findAll().deleteAllFromRealm(); 73 | mRealm.copyToRealm(queries); 74 | mRealm.commitTransaction(); 75 | } 76 | 77 | @Override 78 | public void suggestionQueryChanged(Observable observable) { 79 | mCompositeDisposable.add(observable 80 | .observeOn(AndroidSchedulers.mainThread()) 81 | .subscribe(charSequence -> { 82 | if (charSequence.length() > 0) { 83 | loadSuggestionsFromAPI(charSequence.toString(), false); 84 | } 85 | }) 86 | ); 87 | } 88 | 89 | @Override 90 | public void detachView() { 91 | if (mCompositeDisposable != null && !mCompositeDisposable.isDisposed()) { 92 | mCompositeDisposable.dispose(); 93 | mCompositeDisposable = null; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/utility/Constants.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.utility; 2 | 3 | 4 | import android.view.View; 5 | 6 | import butterknife.ButterKnife.Action; 7 | 8 | public class Constants { 9 | 10 | public static final String BASE_URL_LOKLAK = "https://api.loklak.org/"; 11 | 12 | public static final String KEY = ""; 13 | public static final String SECRET = ""; 14 | 15 | public static final String TWEET_SEARCH_SUGGESTION_QUERY_KEY = "search_suggestion_query"; 16 | public static final String OAUTH_ACCESS_TOKEN_KEY = "oauth_access_token"; 17 | public static final String OAUTH_ACCESS_TOKEN_SECRET_KEY = "oauth_access_token_secret"; 18 | public static final String USER_ID = "user_id"; 19 | public static final String USER_NAME = "user_name"; 20 | public static final String USER_SCREEN_NAME = "user_screen_name"; 21 | public static final String USER_PROFILE_IMAGE_URL = "user_profile_image_url"; 22 | 23 | public static final Action VISIBLE = (view, index) -> view.setVisibility(View.VISIBLE); 24 | public static final Action GONE = (view, index) -> view.setVisibility(View.GONE); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/utility/FileUtils.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.utility; 2 | 3 | import android.content.ContentUris; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | import android.os.Build; 8 | import android.os.Environment; 9 | import android.provider.DocumentsContract; 10 | import android.provider.MediaStore; 11 | 12 | 13 | public class FileUtils { 14 | 15 | final static boolean IS_KITKAT_AND_ABOVE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 16 | 17 | public static boolean isExternalStorageDocument(Uri uri) { 18 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 19 | } 20 | 21 | public static boolean isDownloadsDocument(Uri uri) { 22 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 23 | } 24 | 25 | public static boolean isMediaDocument(Uri uri) { 26 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 27 | } 28 | 29 | public static String getColumnData(Context context, Uri uri, 30 | String selection, String[] selectionArgs) { 31 | final String column = "_data"; 32 | final String[] projection = {column}; 33 | 34 | Cursor cursor = context.getContentResolver().query( 35 | uri, 36 | projection, 37 | selection, 38 | selectionArgs, 39 | null); 40 | if (cursor != null && cursor.moveToFirst()) { 41 | final int index = cursor.getColumnIndex(column); 42 | if (index >= 0) { 43 | return cursor.getString(index); 44 | } 45 | } 46 | if (cursor != null) { 47 | cursor.close(); 48 | } 49 | return null; 50 | } 51 | 52 | public static String getPath(final Context context, final Uri uri) { 53 | if (IS_KITKAT_AND_ABOVE && DocumentsContract.isDocumentUri(context, uri)) { 54 | 55 | // document provider 56 | if (isExternalStorageDocument(uri)) { 57 | final String docId = DocumentsContract.getDocumentId(uri); 58 | final String[] split = docId.split(":"); 59 | final String type = split[0]; 60 | final String id = split[1]; 61 | 62 | if ("primary".equalsIgnoreCase(type)) { 63 | return Environment.getExternalStorageDirectory() + "/" + id; 64 | } 65 | } 66 | // downloads provider 67 | else if (isDownloadsDocument(uri)) { 68 | final String id = DocumentsContract.getDocumentId(uri); 69 | final Uri contentUri = ContentUris.withAppendedId( 70 | Uri.parse("content://downloads/public_downloads"), 71 | Long.parseLong(id)); 72 | return getColumnData(context, contentUri, null, null); 73 | } 74 | // media provider 75 | else if (isMediaDocument(uri)) { 76 | Uri contentUri = null; 77 | final String docId = DocumentsContract.getDocumentId(uri); 78 | final String[] split = docId.split(":"); 79 | final String type = split[0]; 80 | final String id = split[1]; 81 | 82 | if (type.equals("image")) { 83 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 84 | } 85 | else if (type.equals("video")) { 86 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 87 | } 88 | else if (type.equals("audio")) { 89 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 90 | } 91 | 92 | final String selection = "_id=?"; 93 | final String[] selectionArgs = {id}; 94 | return getColumnData(context, contentUri, selection, selectionArgs); 95 | } 96 | // file 97 | else if (uri.getScheme().equalsIgnoreCase("file")) return uri.getPath(); 98 | } 99 | return null; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/org/loklak/wok/utility/SharedPrefUtil.java: -------------------------------------------------------------------------------- 1 | package org.loklak.wok.utility; 2 | 3 | 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | import android.preference.PreferenceManager; 7 | 8 | public class SharedPrefUtil { 9 | 10 | public static String getSharedPrefString(Context context, String key) { 11 | SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); 12 | return sharedPref.getString(key, ""); 13 | } 14 | 15 | public static void setSharedPrefString(Context context, String key, String value) { 16 | SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); 17 | SharedPreferences.Editor editor = sharedPref.edit(); 18 | editor.putString(key, value); 19 | editor.apply(); 20 | } 21 | 22 | public static void removeSharedPrefKey(Context context, String key) { 23 | SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); 24 | SharedPreferences.Editor editor = sharedPref.edit(); 25 | editor.remove(key); 26 | editor.commit(); 27 | } 28 | 29 | public static void clearSharedPrefData(Context context) { 30 | PreferenceManager.getDefaultSharedPreferences(context).edit().clear().apply(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/res/anim/back_button_bottom_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/back_button_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/anim/back_button_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/back_button_top_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/bottom_enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/enter.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/top_exit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/camera.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/heart.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_location_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_gallery_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_location_on_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/loklak_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/loklak_wok_android/a22f75c76f2457aae5eba32f1c1b95d77f458be6/app/src/main/res/drawable/loklak_splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/media_button_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/reply.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/retweet.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_round_corner_tweet_media_remove.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_round_corner_tweet_post.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/tweet_button_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/twitter_pink.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/twitter_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_suggest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_tweet_harvesting.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_tweet_posting.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/content_tweet_harvesting.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 32 | 33 | 41 | 42 | 49 | 50 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fab_tweet_posting.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 16 | 17 | 21 | 22 | 30 | 31 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 55 | 56 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_search_category.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 19 | 20 | 33 | 34 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_suggest.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 17 | 18 | 21 | 22 | 31 | 32 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_tweet_harvesting.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_tweet_posting.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 16 | 17 | 18 | 19 | 27 | 28 | 38 | 39 | 45 | 46 | 47 | 55 | 56 | 57 | 58 | 64 | 65 | 72 | 73 | 82 | 83 | 92 | 93 | 102 | 103 | 111 | 112 |