├── settings.gradle
├── Icon
├── icon.png
└── icon.psd
├── app
├── libs
│ ├── twitter4j-core-4.0.7.jar
│ ├── twitter4j-async-4.0.7.jar
│ ├── twitter4j-stream-4.0.7.jar
│ └── twitter4j-examples-4.0.7.jar
├── src
│ └── main
│ │ ├── res
│ │ ├── drawable
│ │ │ ├── logo.png
│ │ │ ├── oval.xml
│ │ │ ├── roundrect_white_5dp.xml
│ │ │ ├── roundrect_white_opacity.xml
│ │ │ ├── roundrect_white_border.xml
│ │ │ ├── arrow_up_bold_box.xml
│ │ │ ├── chart_areaspline.xml
│ │ │ ├── gradient_title.xml
│ │ │ ├── download.xml
│ │ │ ├── arrow_down_bold_box.xml
│ │ │ ├── chevron_down.xml
│ │ │ ├── chevron_up.xml
│ │ │ ├── check_circle.xml
│ │ │ ├── roundrect_red_border.xml
│ │ │ ├── plus_circle.xml
│ │ │ ├── arrow_right_circle.xml
│ │ │ ├── credit_card_outline.xml
│ │ │ ├── content_save.xml
│ │ │ ├── comment_remove.xml
│ │ │ ├── dots_vertical.xml
│ │ │ ├── calendar_edit.xml
│ │ │ ├── roundrect_white_50dp.xml
│ │ │ ├── content_save_off.xml
│ │ │ ├── account_multiple_remove.xml
│ │ │ ├── link_variant_remove.xml
│ │ │ └── link_variant.xml
│ │ ├── font
│ │ │ ├── noto_sans_bold.otf
│ │ │ ├── noto_sans_light.otf
│ │ │ └── noto_sans_regular.otf
│ │ ├── mipmap
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── drawable-hdpi
│ │ │ └── ic_stat_icon.png
│ │ ├── drawable-mdpi
│ │ │ └── ic_stat_icon.png
│ │ ├── drawable-xhdpi
│ │ │ └── ic_stat_icon.png
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── drawable-xxhdpi
│ │ │ └── ic_stat_icon.png
│ │ ├── drawable-xxxhdpi
│ │ │ └── ic_stat_icon.png
│ │ ├── menu
│ │ │ ├── logic_menu.xml
│ │ │ ├── user_token_item_menu.xml
│ │ │ └── logicpair_menu.xml
│ │ ├── values
│ │ │ ├── dimen.xml
│ │ │ ├── key.xml
│ │ │ ├── attrs.xml
│ │ │ ├── styles.xml
│ │ │ └── colors.xml
│ │ └── layout
│ │ │ ├── full_recycler_view.xml
│ │ │ ├── activity_fav_cleaner.xml
│ │ │ ├── view_rounded_button.xml
│ │ │ ├── view_oval_image.xml
│ │ │ ├── fragment_no_item.xml
│ │ │ ├── fragment_report_list.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_hetzer.xml
│ │ │ ├── fragment_spotlight.xml
│ │ │ ├── fragment_column_header.xml
│ │ │ ├── item_logic.xml
│ │ │ ├── item_default.xml
│ │ │ ├── fragment_title_with_desc.xml
│ │ │ ├── fragment_sasarinomari.xml
│ │ │ ├── fragment_card_button.xml
│ │ │ ├── activity_analytics.xml
│ │ │ ├── item_logicpair.xml
│ │ │ ├── activity_media_download.xml
│ │ │ ├── item_usertoken.xml
│ │ │ ├── view_dashboard_card.xml
│ │ │ ├── item_sku.xml
│ │ │ ├── activity_report_list.xml
│ │ │ ├── item_simpleuser.xml
│ │ │ ├── activity_logic_pair_edit.xml
│ │ │ ├── activity_block_clear.xml
│ │ │ ├── activity_token_management.xml
│ │ │ └── activity_logicpair.xml
│ │ ├── java
│ │ └── com
│ │ │ └── sasarinomari
│ │ │ └── tweeper
│ │ │ ├── FavCleaner
│ │ │ ├── FavCleaner.kt
│ │ │ └── FavCleanerActivity.kt
│ │ │ ├── SimplizatedClass
│ │ │ ├── Status.kt
│ │ │ └── User.kt
│ │ │ ├── TwitterErrorCode.kt
│ │ │ ├── Billing
│ │ │ └── AdRemover.kt
│ │ │ ├── FirebaseLogger.kt
│ │ │ ├── StringFormatter.kt
│ │ │ ├── Base
│ │ │ ├── ActivityRefrashReceiver.kt
│ │ │ └── BaseActivity.kt
│ │ │ ├── Hetzer
│ │ │ ├── HetzerReport.kt
│ │ │ ├── LogicpairTypeSelectActivity.kt
│ │ │ └── HetzerActivity.kt
│ │ │ ├── Tweeper.kt
│ │ │ ├── SystemEventReceiver.kt
│ │ │ ├── NotificationChannels.kt
│ │ │ ├── Analytics
│ │ │ ├── AnalyticsReport.kt
│ │ │ └── AnalyticsNotificationReceiver.kt
│ │ │ ├── View
│ │ │ ├── OvalImageView.kt
│ │ │ ├── DefaultListItem.kt
│ │ │ └── DashboardCardView.kt
│ │ │ ├── Authenticate
│ │ │ ├── TokenManagementActivity.kt
│ │ │ ├── AuthDataAdapter.kt
│ │ │ └── AuthData.kt
│ │ │ ├── ChainBlock
│ │ │ ├── BlockClearActivity.kt
│ │ │ └── BlockClearService.kt
│ │ │ ├── TwitterExceptionHandler.kt
│ │ │ ├── DialogAdapter.kt
│ │ │ ├── Report
│ │ │ ├── ReportListActivity.kt
│ │ │ └── ReportListFragment.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── Permission
│ │ │ └── PermissionHelper.java
│ │ │ ├── DefaultRecycleAdapter.kt
│ │ │ ├── RewardedAdAdapter.kt
│ │ │ ├── MediaDownload
│ │ │ ├── DownloadReceiver.kt
│ │ │ └── MediaDownloadActivity.kt
│ │ │ ├── UITestActivity.kt
│ │ │ ├── RecyclerInjector.kt
│ │ │ └── ScheduledTask
│ │ │ └── ScheduleManageActivity.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── WebView
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── sasarinomari
│ │ └── webview
│ │ ├── WebViewClientInterface.java
│ │ ├── WebChromeClientInterface.java
│ │ ├── WebViewInterface.java
│ │ ├── WebViewLoginAssistant.java
│ │ └── WebViewClient.java
├── build.gradle
└── proguard-rules.pro
├── .gitignore
├── README.md
├── gradle.properties
├── gradlew.bat
├── policy.txt
└── gradlew
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':WebView'
2 |
--------------------------------------------------------------------------------
/Icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/Icon/icon.png
--------------------------------------------------------------------------------
/Icon/icon.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/Icon/icon.psd
--------------------------------------------------------------------------------
/app/libs/twitter4j-core-4.0.7.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/libs/twitter4j-core-4.0.7.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/libs/twitter4j-async-4.0.7.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/libs/twitter4j-async-4.0.7.jar
--------------------------------------------------------------------------------
/app/libs/twitter4j-stream-4.0.7.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/libs/twitter4j-stream-4.0.7.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/drawable/logo.png
--------------------------------------------------------------------------------
/app/libs/twitter4j-examples-4.0.7.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/libs/twitter4j-examples-4.0.7.jar
--------------------------------------------------------------------------------
/app/src/main/res/font/noto_sans_bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/font/noto_sans_bold.otf
--------------------------------------------------------------------------------
/app/src/main/res/font/noto_sans_light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/font/noto_sans_light.otf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/mipmap/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/font/noto_sans_regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/font/noto_sans_regular.otf
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/FavCleaner/FavCleaner.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.FavCleaner
2 |
3 | class FavCleaner {
4 | }
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_stat_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/drawable-hdpi/ic_stat_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_stat_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/drawable-mdpi/ic_stat_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_stat_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/drawable-xhdpi/ic_stat_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_stat_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/drawable-xxhdpi/ic_stat_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxxhdpi/ic_stat_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SasarinoMARi/Tweeper/HEAD/app/src/main/res/drawable-xxxhdpi/ic_stat_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/menu/logic_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/oval.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/WebView/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/roundrect_white_5dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/user_token_item_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/WebView/src/main/java/com/sasarinomari/webview/WebViewClientInterface.java:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.webview;
2 |
3 | public interface WebViewClientInterface
4 | {
5 | void onPageFinished( String url );
6 | boolean shouldOverrideUrlLoading( String url );
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/roundrect_white_opacity.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Jun 21 15:13:11 KST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/SimplizatedClass/Status.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.SimplizatedClass
2 |
3 | import java.util.*
4 |
5 | class Status(src: twitter4j.Status) {
6 | val id: Long = src.id
7 | val text: String = src.text
8 | val createdAt: Date = src.createdAt
9 | }
--------------------------------------------------------------------------------
/WebView/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 28
5 |
6 | defaultConfig {
7 | minSdkVersion 15
8 | targetSdkVersion 28
9 | }
10 | lintOptions {
11 | abortOnError false
12 | }
13 | }
14 |
15 | dependencies {
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/TwitterErrorCode.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | enum class TwitterStatusCode(val code: Int) {
4 | OK(20), NotFound(404), Unauthrized(403), RateLimitExceeded(429)
5 | }
6 | enum class TwitterErrorCode(val code: Int) {
7 | RateLlimitExceeded(88), UserNotFound(50)
8 | }
--------------------------------------------------------------------------------
/app/src/main/res/menu/logicpair_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/roundrect_white_border.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 10dp
5 | 16dp
6 | 10dp
7 |
--------------------------------------------------------------------------------
/WebView/src/main/java/com/sasarinomari/webview/WebChromeClientInterface.java:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.webview;
2 |
3 | import android.content.Intent;
4 |
5 | /**
6 | * Created by MARi on 2018-01-26.
7 | */
8 |
9 | public interface WebChromeClientInterface
10 | {
11 | void onStartActivityForResult( Intent intent, int RequestCode );
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/full_recycler_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_up_bold_box.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/chart_areaspline.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/gradient_title.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/download.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_down_bold_box.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/chevron_down.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/chevron_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/check_circle.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/roundrect_red_border.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/plus_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_right_circle.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/credit_card_outline.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/FavCleaner/FavCleanerActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.FavCleaner
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import android.os.Bundle
5 | import com.sasarinomari.tweeper.R
6 |
7 | class FavCleanerActivity : AppCompatActivity() {
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 | setContentView(R.layout.activity_fav_cleaner)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/content_save.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_fav_cleaner.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/comment_remove.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dots_vertical.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/calendar_edit.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/roundrect_white_50dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 |
6 |
7 |
8 |
9 |
10 | -
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/SimplizatedClass/User.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.SimplizatedClass
2 |
3 | class User(src: twitter4j.User) {
4 | val name: String = src.name
5 | val screenName: String = src.screenName
6 | val profileImageUrl: String = src.profileImageURLHttps
7 | val id: Long = src.id
8 | val bio: String = src.description
9 |
10 | override fun equals(other: Any?): Boolean {
11 | return if (other is User) this.id == other.id else super.equals(other)
12 | }
13 |
14 | override fun hashCode(): Int {
15 | return id.hashCode()
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Billing/AdRemover.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Billing
2 |
3 | import android.content.Context
4 |
5 | class AdRemover(private val context: Context) {
6 | private val key = "ADRemoved"
7 |
8 | fun isAdRemoved() : Boolean {
9 | val prefs = context.getSharedPreferences(key, Context.MODE_PRIVATE)
10 | val flag = prefs.getInt("flag", 0)
11 | return flag == 1
12 | }
13 | internal fun removeAd(){
14 | val prefs = context.getSharedPreferences(key, Context.MODE_PRIVATE).edit()
15 | prefs.putInt("flag", 1)
16 | prefs.apply()
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/content_save_off.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/FirebaseLogger.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import com.google.firebase.analytics.FirebaseAnalytics
6 |
7 | class FirebaseLogger(context: Context) {
8 | private val firebaseAnalytics = FirebaseAnalytics.getInstance(context)
9 |
10 | fun log(eventName: String, vararg parameters: Pair) {
11 | val bundle = Bundle()
12 | for(param in parameters) {
13 | bundle.putString(param.first, param.second)
14 | }
15 | firebaseAnalytics.logEvent(eventName, bundle)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/key.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ca-app-pub-3940256099942544/6300978111
9 |
10 | ca-app-pub-3940256099942544/5224354917
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/StringFormatter.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | class StringFormatter {
4 | companion object {
5 | fun extractionString(string: String, startKeyword: String?, endKeyword: String?): String? {
6 | var startIndex = 0
7 | if (startKeyword != null) startIndex = string.indexOf(startKeyword) + startKeyword.length
8 | var endIndex = string.length
9 | if (endKeyword != null) endIndex = string.indexOf(endKeyword, startIndex)
10 | if (endIndex < 0) return null // 트위터 인증 거부한 경우
11 | return string.substring(startIndex, endIndex)
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Base/ActivityRefrashReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Base
2 |
3 | import android.app.Activity
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 |
8 | class ActivityRefrashReceiver(private val activity: Activity): BroadcastReceiver() {
9 | companion object {
10 | const val eventName = "Tweeper_Refrash_Activity"
11 | }
12 | enum class Parameters {
13 | Target
14 | }
15 |
16 | override fun onReceive(p0: Context?, p1: Intent?) {
17 | val target = p1!!.getStringExtra(Parameters.Target.name)
18 | if(target == activity::class.java.name) activity.recreate()
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_rounded_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Hetzer/HetzerReport.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Hetzer
2 |
3 | import java.util.*
4 | import com.sasarinomari.tweeper.SimplizatedClass.Status
5 |
6 | class HetzerReport {
7 | constructor()
8 | constructor(removedStatuses: List, savedStatuses: List) {
9 | for(status in removedStatuses) this.removedStatuses.add(Status(status))
10 | for(status in savedStatuses) this.savedStatuses.add(Status(status))
11 | }
12 |
13 | companion object {
14 | const val prefix = "hrv2_" // hetzer report version 2 _
15 | }
16 |
17 | var id: Int = -1
18 | var removedStatuses = ArrayList()
19 | var savedStatuses = ArrayList()
20 | var date = Date(0)
21 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/account_multiple_remove.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Tweeper.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.app.Application
4 |
5 | class Tweeper : Application() {
6 | enum class RequestCodes {
7 | ScheduledAnalytics
8 | }
9 |
10 | companion object DataHolder {
11 | private var map : HashMap = hashMapOf()
12 | fun loadData(key: String, data: Any) {
13 | map[key] = data
14 | }
15 |
16 | fun getData(key:String) : Any? {
17 | if(!hasData(key)) return null
18 | return map[key];
19 | }
20 |
21 | fun dropData(key:String){
22 | map.remove(key)
23 | }
24 |
25 | fun hasData(key: String): Boolean {
26 | return map.containsKey(key)
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_oval_image.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_no_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/WebView/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | /*/build/
3 |
4 | # Crashlytics configuations
5 | com_crashlytics_export_strings.xml
6 |
7 | # Local configuration file (sdk path, etc)
8 | local.properties
9 |
10 | # Gradle generated files
11 | .gradle/
12 |
13 | # Signing files
14 | .signing/
15 |
16 | # User-specific configurations
17 | .idea/
18 | .idea/libraries/
19 | .idea/workspace.xml
20 | .idea/tasks.xml
21 | .idea/.name
22 | .idea/compiler.xml
23 | .idea/copyright/profiles_settings.xml
24 | .idea/encodings.xml
25 | .idea/misc.xml
26 | .idea/modules.xml
27 | .idea/scopes/scope_settings.xml
28 | .idea/vcs.xml
29 | *.iml
30 |
31 | # OS-specific files
32 | .DS_Store
33 | .DS_Store?
34 | ._*
35 | .Spotlight-V100
36 | .Trashes
37 | ehthumbs.db
38 | Thumbs.db
39 |
40 | # Custom Igonre Files
41 | session
42 |
43 | **/build/
44 | *.keystore
45 | **/release/
46 |
47 | live/
48 | dev/
--------------------------------------------------------------------------------
/app/src/main/res/drawable/link_variant_remove.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -keep class su.levenetc.android.textsurface.** { *; }
--------------------------------------------------------------------------------
/WebView/src/main/java/com/sasarinomari/webview/WebViewInterface.java:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.webview;
2 |
3 | import android.util.Log;
4 | import android.webkit.JavascriptInterface;
5 | import android.webkit.WebView;
6 |
7 | /**
8 | * Created by MARi on 2018-01-26.
9 | */
10 |
11 | public class WebViewInterface
12 | {
13 | WebView webView;
14 |
15 | public WebViewInterface(WebView webView) {this.webView = webView;}
16 |
17 | @JavascriptInterface
18 | public void applyLoginInfo(String id, String pw) {
19 | Log.i( "BoomWebViewInterface", "applyLoginInfo" );
20 | WebViewLoginAssistant.setAutoLoginParameters(webView.getContext(), id, pw);
21 | }
22 |
23 | @JavascriptInterface
24 | public void removeLoginInfo() {
25 | Log.i( "BoomWebViewInterface", "removeLoginInfo" );
26 | WebViewLoginAssistant.removeAutoLoginParameters(webView.getContext());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/SystemEventReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.util.Log
7 | import com.sasarinomari.tweeper.Analytics.AnalyticsActivity
8 | import com.sasarinomari.tweeper.Analytics.AnalyticsNotificationReceiver
9 |
10 | class SystemEventReceiver : BroadcastReceiver() {
11 | override fun onReceive(context: Context, intent: Intent) {
12 | when(intent.action){
13 | Intent.ACTION_BOOT_COMPLETED -> {
14 | /**
15 | * 재부팅됨
16 | */
17 | if(BuildConfig.DEBUG) Log.d("SystemEventReceiver", "단말기 재부팅됨!")
18 | val analyticsScheduled = AnalyticsNotificationReceiver.isApplied(context)
19 | AnalyticsNotificationReceiver.apply(context, analyticsScheduled)
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/link_variant.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 | #83D5ff
5 | #73c5f7
6 | #01579B
7 | #139df1
8 | #dF000000
9 | #2Fffffff
10 | #1Fffffff
11 | #2F808080
12 | #000000
13 | #37FADC
14 | #cccccc
15 | #808080
16 | #f44336
17 | #00CF91
18 | #64b5f6
19 | #ba68c8
20 | #e53935
21 | #FFD600
22 | #546e7a
23 |
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tweeper
2 | 트윗지기는 트윗 청소기, 트윗 일지 작성, 미디어 다운로드, 체인블락 등의 유용한 기능을 포함한 안드로이드 앱입니다.
3 |
4 | 
5 | 
6 | 
7 | 
8 | 
9 |
10 | 현재 구현된 기능
11 | - 트윗 청소기 : 원하는 조건에 해당하는 트윗만 남기고 삭제합니다.
12 | - 트윗 일지 작성 : 트윗 수와 팔로우 변화 측정, 언팔매니저 기능 포함.
13 | - 미디어 다운로더 : 트윗에 포함된 사진, Gif 이미지, 동영상을 다운로드합니다.
14 | - 체인 블락 : 특정 유저의 팔로잉 또는 팔로워를 전부 블락합니다.
15 | - 블락 일괄 해제 : 로그인한 계정의 블락 목록을 초기화합니다.
16 |
17 | 앞으로 구현할 기능
18 | - 예약 작업 : 지정된 시간에 트윗지기의 기능을 실행합니다.
19 | - 아카이브 트윗 청소기 : 트위터 아카이브 파일을 사용해 트윗을 삭제합니다.
20 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/NotificationChannels.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.app.NotificationChannel
4 | import android.app.NotificationManager
5 | import android.content.Context
6 | import android.os.Build
7 | import androidx.annotation.RequiresApi
8 | import com.sasarinomari.tweeper.Base.BaseService
9 |
10 | class NotificationChannels {
11 | fun declaration(context: Context) {
12 | if (Build.VERSION.SDK_INT >= 26) {
13 | val mNotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
14 | mNotificationManager.createNotificationChannel(generalChannel(context))
15 | mNotificationManager.createNotificationChannel(serviceChannel(context))
16 | }
17 | }
18 |
19 | @RequiresApi(Build.VERSION_CODES.O)
20 | private fun generalChannel(context: Context): NotificationChannel {
21 | return NotificationChannel("General", context.getString(R.string.General), NotificationManager.IMPORTANCE_DEFAULT)
22 | }
23 |
24 | @RequiresApi(Build.VERSION_CODES.O)
25 | private fun serviceChannel(context: Context): NotificationChannel {
26 | return NotificationChannel("Service", context.getString(R.string.Service), NotificationManager.IMPORTANCE_LOW)
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_report_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
26 |
27 |
28 |
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
20 |
21 |
26 |
27 |
28 |
29 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_hetzer.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
14 |
22 |
23 |
26 |
27 |
32 |
33 |
34 |
35 |
41 |
--------------------------------------------------------------------------------
/WebView/src/main/java/com/sasarinomari/webview/WebViewLoginAssistant.java:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.webview;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.SharedPreferences;
6 |
7 | /**
8 | * Created by MARi on 2018-01-26.
9 | */
10 |
11 | public class WebViewLoginAssistant
12 | {
13 | private static String preferenceName = ".WebViewLoginInfo";
14 |
15 | public static String getAutoLoginParameters( Context context )
16 | {
17 | SharedPreferences pref = context.getSharedPreferences( context.getPackageName() + preferenceName, Activity.MODE_PRIVATE );
18 | String id = pref.getString( "id", "" );
19 | String pw = pref.getString( "pw", "" );
20 | if ( "".equals( id ) || "".equals( pw ) )
21 | return null;
22 | return "id=" + id + "&pw=" + pw;
23 | }
24 |
25 | public static void setAutoLoginParameters( Context context, String id, String pw )
26 | {
27 | SharedPreferences pref = context.getSharedPreferences( context.getPackageName() + preferenceName, Activity.MODE_PRIVATE );
28 | SharedPreferences.Editor editor = pref.edit( );
29 | editor.putString( "id", id );
30 | editor.putString( "pw", pw );
31 | editor.apply( );
32 | }
33 |
34 | public static void removeAutoLoginParameters( Context context )
35 | {
36 | SharedPreferences pref = context.getSharedPreferences( context.getPackageName() + preferenceName, Activity.MODE_PRIVATE );
37 | SharedPreferences.Editor editor = pref.edit( );
38 | editor.remove( "id" );
39 | editor.remove( "pw" );
40 | editor.apply( );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_spotlight.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
23 |
24 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_column_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
18 |
19 |
23 |
24 |
33 |
34 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_logic.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
28 |
29 |
30 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_default.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
20 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Analytics/AnalyticsReport.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Analytics
2 |
3 | import com.sasarinomari.tweeper.SimplizatedClass.User
4 | import java.util.*
5 | import kotlin.collections.ArrayList
6 |
7 | internal class AnalyticsReport {
8 | constructor()
9 | constructor(me: twitter4j.User,
10 | followings: List,
11 | followers: List,
12 | previousReport: AnalyticsReport? = null) {
13 | this.date = Date()
14 | this.userId = me.id
15 | this.tweetCount = me.statusesCount
16 | for(user in followings) this.followings.add(User(user))
17 | for(user in followers) this.followers.add(User(user))
18 | if(previousReport!=null) setDeffrence(previousReport)
19 | }
20 |
21 | companion object {
22 | const val prefix = "analyticsReport"
23 | }
24 |
25 | var id: Int = -1
26 |
27 | var date = Date(0)
28 | var userId: Long = -1
29 | var tweetCount: Int = -1
30 | var followings = ArrayList()
31 | var followers = ArrayList()
32 |
33 | var tweetCountVar: Int? = null
34 | var followingsVar: Int? = null
35 | var followersVar: Int? = null
36 |
37 | fun setDeffrence(previousReport: AnalyticsReport) {
38 | this.tweetCountVar = this.tweetCount - previousReport.tweetCount
39 | this.followingsVar = this.followings.count() - previousReport.followings.count()
40 | this.followersVar = this.followers.count() - previousReport.followers.count()
41 | }
42 |
43 | override operator fun equals(other: Any?): Boolean {
44 | return if (other is AnalyticsReport) this.userId == other.userId else super.equals(other)
45 | }
46 |
47 | override fun hashCode(): Int {
48 | return userId.hashCode()
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Hetzer/LogicpairTypeSelectActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Hetzer
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import com.sasarinomari.tweeper.Base.BaseActivity
6 | import com.sasarinomari.tweeper.R
7 | import kotlinx.android.synthetic.main.activity_logicpair_type_select.*
8 | import kotlinx.android.synthetic.main.fragment_title_with_desc.view.*
9 |
10 | class LogicpairTypeSelectActivity : BaseActivity() {
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | setContentView(R.layout.activity_logicpair_type_select)
14 |
15 | layout_title_and_desc.title_text.text = getString(R.string.TweetCleaner)
16 | layout_title_and_desc.title_description.text = getString(R.string.TweetCleanerDescription)
17 |
18 | button_addSaveRule.setOnClickListener {
19 | val i = Intent(this@LogicpairTypeSelectActivity, LogicPairEditActivity::class.java)
20 | i.putExtra(LogicPairEditActivity.Parameters.LogicType.name, LogicPair.LogicType.Save.ordinal)
21 | startActivityForResult(i, 0)
22 | }
23 |
24 | button_addRemoveRule.setOnClickListener {
25 | val i = Intent(this@LogicpairTypeSelectActivity, LogicPairEditActivity::class.java)
26 | i.putExtra(LogicPairEditActivity.Parameters.LogicType.name, LogicPair.LogicType.Remove.ordinal)
27 | startActivityForResult(i, 0)
28 | }
29 | }
30 |
31 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
32 | if(requestCode == 0) {
33 | if(resultCode == RESULT_OK) {
34 | setResult(RESULT_OK, data)
35 | finish()
36 | }
37 | }
38 | else super.onActivityResult(requestCode, resultCode, data)
39 | }
40 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_title_with_desc.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
30 |
31 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/View/OvalImageView.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.View
2 |
3 | import android.content.Context
4 | import android.content.res.TypedArray
5 | import android.graphics.drawable.GradientDrawable
6 | import android.util.AttributeSet
7 | import android.view.LayoutInflater
8 | import android.widget.RelativeLayout
9 | import androidx.core.content.ContextCompat
10 | import com.sasarinomari.tweeper.R
11 | import kotlinx.android.synthetic.main.view_oval_image.view.*
12 |
13 | class OvalImageView : RelativeLayout{
14 | constructor(context: Context) : super(context) {
15 | init(null, 0)
16 | }
17 |
18 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
19 | init(attrs, 0)
20 | }
21 |
22 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
23 | context,
24 | attrs,
25 | defStyle
26 | ) {
27 | init(attrs, defStyle)
28 | }
29 |
30 | private fun init(attrs: AttributeSet?, defStyle: Int) {
31 | LayoutInflater.from(context).inflate(R.layout.view_oval_image, this, true)
32 | this.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent))
33 | val typedArray = context.obtainStyledAttributes(attrs, R.styleable.OvalImageView, defStyle, 0)
34 | setTypeArray(typedArray)
35 | }
36 |
37 |
38 | private fun setTypeArray(typedArray: TypedArray) {
39 | val iconAttr = typedArray.getResourceId(R.styleable.OvalImageView_icon, 0)
40 | val ovalAttr = typedArray.getColor(R.styleable.OvalImageView_ovalColor, 0)
41 |
42 | if(iconAttr!=0) setImageResource(iconAttr)
43 | if(ovalAttr!=0) setOvalColor(ovalAttr)
44 |
45 | typedArray.recycle()
46 | }
47 |
48 | fun setImageResource(imageId: Int) { _oi_image.setImageResource(imageId) }
49 |
50 | fun setOvalColor(color: Int) { (_oi_oval.drawable as GradientDrawable).setColor(color) }
51 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_sasarinomari.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
18 |
19 |
25 |
26 |
34 |
35 |
45 |
46 |
47 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_card_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
19 |
20 |
30 |
31 |
37 |
38 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/View/DefaultListItem.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.View
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.util.TypedValue
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.BaseAdapter
10 | import android.widget.TextView
11 | import com.google.gson.Gson
12 | import com.sasarinomari.tweeper.R
13 | import kotlinx.android.synthetic.main.item_default.view.*
14 |
15 | @Suppress("DEPRECATION")
16 | @Deprecated("Use RecyclerInjector")
17 | abstract class DefaultListItem(private val items: List<*>) : BaseAdapter() {
18 | var clickEffectVisibility = false
19 |
20 | override fun getCount(): Int {
21 | return items.size
22 | }
23 |
24 | @SuppressLint("SetTextI18n", "SimpleDateFormat")
25 | override fun getView(position: Int, _convertView: View?, parent: ViewGroup): View {
26 | var convertView = _convertView
27 | val context = parent.context
28 |
29 | if (convertView == null) {
30 | val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
31 | convertView = inflater.inflate(R.layout.item_default, parent, false)
32 | }
33 |
34 | convertView!!
35 | drawItem(items[position]!!, convertView.defaultitem_title, convertView.defaultitem_description)
36 |
37 | if(clickEffectVisibility) { // 이거 없어도 클릭 이펙트 잘 나오는데 왜 씀? ㅋㅋ
38 | val outValue = TypedValue()
39 | context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true)
40 | convertView.setBackgroundResource(outValue.resourceId)
41 | }
42 | else {
43 | convertView.background = null
44 | }
45 |
46 | return convertView
47 | }
48 |
49 | abstract fun drawItem(item: Any, title: TextView, description: TextView)
50 |
51 | override fun getItemId(position: Int): Long {
52 | return position.toLong()
53 | }
54 |
55 | override fun getItem(position: Int): Any? {
56 | return items[position]
57 | }
58 |
59 | fun getItemToJson(position: Int): String? {
60 | return Gson().toJson(getItem(position))
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_analytics.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
19 |
20 |
23 |
24 |
29 |
30 |
31 |
32 |
40 |
41 |
51 |
52 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_logicpair.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
16 |
17 |
25 |
26 |
35 |
36 |
37 |
42 |
43 |
44 |
48 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Authenticate/TokenManagementActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Authenticate
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import com.google.gson.Gson
7 | import com.sasarinomari.tweeper.Base.BaseActivity
8 | import com.sasarinomari.tweeper.R
9 | import com.sasarinomari.tweeper.TwitterAdapter
10 | import kotlinx.android.synthetic.main.activity_token_management.*
11 |
12 | class TokenManagementActivity : BaseActivity() {
13 | companion object {
14 | val RESULT_AUTH_DATA = "authData"
15 | }
16 |
17 | private val recorder = AuthData.Recorder(this)
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | setContentView(R.layout.activity_token_management)
22 |
23 | getAuthData()
24 | button_addContition.setOnClickListener {
25 | startActivityForResult(Intent(this, AuthenticationActivity::class.java), 0)
26 | }
27 | }
28 |
29 | private fun getAuthData() {
30 | val users = recorder.getUsers()
31 | if (users.isEmpty()) {
32 | listView.visibility = View.GONE
33 | } else {
34 | val adapter = AuthDataAdapter(users, object : AuthDataAdapter.ActivityInterface {
35 | override fun onSelectUser(authData: AuthData) {
36 | recorder.setFocusedUser(authData)
37 | setResult(RESULT_OK)
38 | finish()
39 | }
40 |
41 | override fun onDeleteUser(authData: AuthData) {
42 | // 저장된 유저를 삭제하고 액티비티를 닫을 때 유저를 갱신하도록 설정
43 | // focused user가 삭제될 경우에 대비한 것임
44 | recorder.deleteUser(authData)
45 | setResult(RESULT_OK)
46 | getAuthData()
47 | }
48 | })
49 | listView.adapter = adapter
50 | listView.visibility = View.VISIBLE
51 | }
52 | }
53 |
54 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
55 | when (requestCode) {
56 | 0 -> {
57 | if (resultCode == RESULT_OK) {
58 | setResult(RESULT_OK)
59 | finish()
60 | }
61 | }
62 | else -> super.onActivityResult(requestCode, resultCode, data)
63 | }
64 | }
65 |
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/ChainBlock/BlockClearActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.ChainBlock
2 |
3 | import android.content.Intent
4 | import android.os.Build
5 | import android.os.Bundle
6 | import com.google.gson.Gson
7 | import com.sasarinomari.tweeper.Authenticate.AuthData
8 | import com.sasarinomari.tweeper.Base.BaseActivity
9 | import com.sasarinomari.tweeper.R
10 | import com.sasarinomari.tweeper.RewardedAdAdapter
11 | import kotlinx.android.synthetic.main.activity_block_clear.*
12 |
13 | class BlockClearActivity : BaseActivity() {
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_block_clear)
17 |
18 | button_ok.setOnClickListener {
19 | if (BlockClearService.checkServiceRunning((this@BlockClearActivity))) {
20 | da.warning(getString(R.string.Wait), getString(R.string.duplicateService_BlockClear)).show()
21 | return@setOnClickListener
22 | }
23 |
24 | da.warning(getString(R.string.AreYouSure), getString(R.string.ActionDoNotRestore))
25 | .setConfirmText(getString(R.string.Yes))
26 | .setCancelText(getString(R.string.Wait))
27 | .setConfirmClickListener {
28 | it.dismissWithAnimation()
29 | RewardedAdAdapter.show(this@BlockClearActivity, object: RewardedAdAdapter.RewardInterface {
30 | override fun onFinished() {
31 | val intent = Intent(this@BlockClearActivity, BlockClearService::class.java)
32 | intent.putExtra(
33 | BlockClearService.Parameters.User.name,
34 | Gson().toJson(AuthData.Recorder(this@BlockClearActivity).getFocusedUser()!!))
35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
36 | startForegroundService(intent)
37 | } else {
38 | startService(intent)
39 | }
40 | da.success(getString(R.string.Done), getString(R.string.BlockClearRunning))
41 | .setConfirmClickListener { it2 ->
42 | it2.dismissWithAnimation()
43 | finish()
44 | }.show()
45 | }
46 | })
47 | }.show()
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_media_download.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
20 |
21 |
22 |
33 |
34 |
35 |
42 |
43 |
50 |
51 |
52 |
53 |
59 |
60 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_usertoken.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
19 |
20 |
25 |
26 |
31 |
32 |
41 |
42 |
51 |
52 |
53 |
54 |
58 |
59 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_dashboard_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
27 |
28 |
41 |
42 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_sku.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
19 |
20 |
28 |
29 |
39 |
40 |
41 |
42 |
51 |
52 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_report_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
21 |
22 |
23 |
32 |
33 |
43 |
44 |
45 |
46 |
52 |
53 |
61 |
62 |
63 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/View/DashboardCardView.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.View
2 |
3 | import android.content.Context
4 | import android.content.res.TypedArray
5 | import android.util.AttributeSet
6 | import android.view.LayoutInflater
7 | import androidx.cardview.widget.CardView
8 | import androidx.core.content.ContextCompat
9 | import com.sasarinomari.tweeper.R
10 | import kotlinx.android.synthetic.main.view_dashboard_card.view.*
11 |
12 | class DashboardCardView : CardView {
13 | constructor(context: Context) : super(context) {
14 | init(null, 0)
15 | }
16 |
17 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
18 | init(attrs, 0)
19 | }
20 |
21 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
22 | context,
23 | attrs,
24 | defStyle
25 | ) {
26 | init(attrs, defStyle)
27 | }
28 |
29 | private fun init(attrs: AttributeSet?, defStyle: Int) {
30 | LayoutInflater.from(context).inflate(R.layout.view_dashboard_card, this, true)
31 | this.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent))
32 | val typedArray = context.obtainStyledAttributes(attrs, R.styleable.DashboardCardView, defStyle, 0)
33 | setTypeArray(typedArray)
34 | }
35 |
36 | private fun setTypeArray(typedArray: TypedArray) {
37 | val textAttr = typedArray.getString(R.styleable.DashboardCardView_title)
38 | val descAttr = typedArray.getString(R.styleable.DashboardCardView_description)
39 | val icon = typedArray.getResourceId(R.styleable.DashboardCardView_icon, 0)
40 | val ovalColor = typedArray.getColor(R.styleable.DashboardCardView_ovalColor, 0)
41 |
42 | card_title.text = textAttr
43 | card_description.text = descAttr
44 | card_oval.setImageResource(icon)
45 | card_oval.setOvalColor(ovalColor)
46 |
47 | /**
48 | * 이름 관련 해괴한 버그가 발견되어서 메모를 남긴다.
49 | * 코틀린 컴파일러 버전 : 1.3.72
50 | *
51 | * 위의 oval 변수는 CustomView인 OvalImageView의 인스턴스이다.
52 | * CustomView in CustomView 상황이기 때문에 어트리뷰트 전달을 위해서 초기화 함수를 만들었다.
53 | * OvalImageView.setOvalColor(int) 함수는 view_oval_image.xml에서 id가 oval인 View에 색을 입히는 함수로 만들어졌다.
54 | *
55 | * 그런데 지금 이 클래스에서도 id가 oval인 View가 있다.
56 | * 이 CustomView에서 inflate하는 레이아웃인 view_dashboard_card.xml에서도 OvalImageView CustomView의 인스턴스 id를 oval로 정의해 두었다.
57 | *
58 | * 자, 이름이 shadowed 된 지금 상황에서 oval.setOvalColor(int)를 호출하자,
59 | * 해당 함수 안의 oval.drawable as GradientDrawable 코드에서 형변환 에러가 발생했다.
60 | *
61 | * 어떻게 이게 가능한지는 잘 모르겠지만 view_oval_image.xml 에서 oval의 이름을 겹치지 않게 변경하는 것으로 수정했다.
62 | */
63 |
64 | typedArray.recycle()
65 | }
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/TwitterExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.util.Log
4 | import twitter4j.TwitterException
5 |
6 | abstract class TwitterExceptionHandler(private val te: TwitterException,
7 | private val apiEndPoint: String) {
8 |
9 | private val LOG_HEADER = "TwitterExceptionHandler"
10 |
11 | fun catch() {
12 | when (te.statusCode) {
13 | TwitterStatusCode.NotFound.code -> {
14 | onNotFound()
15 | }
16 | TwitterStatusCode.Unauthrized.code -> {
17 | onNotFound()
18 | }
19 | else -> {
20 | when (te.errorCode) {
21 | TwitterErrorCode.RateLlimitExceeded.code -> {
22 | onRateLimitExceeded()
23 | Log.i(LOG_HEADER,
24 | "Rate Limit Exceeded:\n\t[API] $apiEndPoint\n\tSeconds Until Reset: ${te.rateLimitStatus.secondsUntilReset}"
25 | )
26 |
27 | if (te.rateLimitStatus.secondsUntilReset > 0)
28 | try {
29 | /**
30 | * 이유는 모르겠으나 sleep 도중 스레드가 종료되어서 Interrupted Exception 발생하는 듯 함.
31 | * 4월 26일 버전에서 우선 이렇게 예외처리함. 이후에도 같은 문제 발생하면 조치 필요
32 | */
33 | Thread.sleep((1000 * te.rateLimitStatus.secondsUntilReset).toLong())
34 | } catch (ex: InterruptedException) {
35 | ex.printStackTrace()
36 | } finally {
37 | onRateLimitReset()
38 | }
39 | }
40 | TwitterErrorCode.UserNotFound.code -> {
41 | onNotFound()
42 | }
43 | -1 -> {
44 | when (te.message) {
45 | "thread interrupted" -> {
46 | Log.i(LOG_HEADER, "Thread Interrupted!")
47 | }
48 | "Unable to resolve host \"api.twitter.com\": No address associated with hostname" -> {
49 | onNetworkError()
50 | }
51 | else -> onUncaughtError()
52 | }
53 | }
54 | else -> onUncaughtError()
55 | }
56 | }
57 | }
58 | }
59 |
60 | abstract fun onNetworkError()
61 | abstract fun onUncaughtError()
62 | abstract fun onRateLimitExceeded()
63 | abstract fun onRateLimitReset()
64 | open fun onNotFound() { }
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/DialogAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.app.Activity
4 | import android.content.DialogInterface
5 | import android.util.TypedValue
6 | import android.view.Gravity
7 | import android.view.View
8 | import android.widget.TextView
9 | import cn.pedant.SweetAlert.SweetAlertDialog
10 |
11 | class DialogAdapter(private val activity: Activity) {
12 | fun message(title: String?, content: String, callback: (()->Unit)? = null): SweetAlertDialog {
13 | val d = SweetAlertDialog(activity, SweetAlertDialog.NORMAL_TYPE)
14 | initialize(d, title, content, callback)
15 | return d
16 | }
17 |
18 | fun error(title: String?, content: String, callback: (()->Unit)? = null): SweetAlertDialog {
19 | val d = SweetAlertDialog(activity, SweetAlertDialog.ERROR_TYPE)
20 | initialize(d, title, content, callback)
21 | return d
22 | }
23 |
24 | fun warning(title: String?, content: String, callback: (()->Unit)? = null): SweetAlertDialog {
25 | val d = SweetAlertDialog(activity, SweetAlertDialog.WARNING_TYPE)
26 | initialize(d, title, content, callback)
27 | return d
28 | }
29 |
30 | fun progress(title: String?, content: String, callback: (()->Unit)? = null): SweetAlertDialog {
31 | val d = SweetAlertDialog(activity, SweetAlertDialog.PROGRESS_TYPE)
32 | initialize(d, title, content, callback)
33 | d.setCancelable(false)
34 | return d
35 | }
36 |
37 | fun success(title: String?, content: String, callback: (()->Unit)? = null): SweetAlertDialog {
38 | val d = SweetAlertDialog(activity, SweetAlertDialog.SUCCESS_TYPE)
39 | initialize(d, title, content, callback)
40 | return d
41 | }
42 |
43 | private fun initialize(d: SweetAlertDialog, title: String?, content: String, callback: (()->Unit)? = null) {
44 | if(title != null) d.titleText = title
45 | d.contentText = content
46 | d.setOnShowListener { dialog ->
47 | dialog as SweetAlertDialog
48 | val titleView: TextView = dialog.findViewById(R.id.title_text) as TextView
49 | titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 19f)
50 | val contentView: TextView = dialog.findViewById(R.id.content_text) as TextView
51 | contentView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
52 | // val confirmView: TextView = dialog.findViewById(R.id.confirm_button) as TextView
53 | // confirmView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f)
54 | // val cancelView: TextView = dialog.findViewById(R.id.cancel_button) as TextView
55 | // cancelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f)
56 | }
57 | d.setOnDismissListener {
58 | if(callback!=null) callback()
59 | }
60 | }
61 |
62 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'com.google.gms.google-services'
5 | apply plugin: 'com.google.firebase.crashlytics'
6 | android {
7 | compileSdkVersion 29
8 | defaultConfig {
9 | applicationId "com.sasarinomari.tweeper"
10 | minSdkVersion 24
11 | targetSdkVersion 29
12 | versionCode 26
13 | versionName "1.26:210427"
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 | buildTypes {
17 | debug {
18 | applicationIdSuffix ".dev"
19 | }
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | }
30 |
31 | dependencies {
32 | implementation fileTree(dir: 'libs', include: ['*.jar'])
33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
34 | implementation 'androidx.appcompat:appcompat:1.1.0'
35 | implementation 'androidx.core:core-ktx:1.3.0'
36 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
37 | testImplementation 'junit:junit:4.12'
38 | androidTestImplementation 'androidx.test:runner:1.2.0'
39 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
40 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
41 |
42 | implementation project(':WebView')
43 | implementation 'com.github.kittinunf.fuel:fuel:+' // Http 클라이언트
44 | implementation 'com.google.code.gson:gson:2.8.6' // Json
45 | implementation 'com.google.android.gms:play-services-ads:19.2.0' // 광고
46 |
47 | // region Firebase
48 | implementation 'com.google.firebase:firebase-analytics:17.4.3'
49 | implementation 'com.google.firebase:firebase-crashlytics:17.1.0'
50 | // endregion
51 |
52 | // region In app 결제
53 | implementation 'com.anjlab.android.iab.v3:library:1.0.44'
54 | // endregion
55 |
56 | // region UI
57 | implementation "com.google.android.material:material:1.0.0"
58 | implementation 'com.afollestad.material-dialogs:core:3.1.1'
59 | implementation 'com.afollestad.material-dialogs:input:3.1.1'
60 | implementation 'com.afollestad.material-dialogs:bottomsheets:3.1.1'
61 | implementation 'com.github.elevenetc:textsurface:0.9.1'
62 | implementation 'com.github.f0ris.sweetalert:library:1.5.6' // 예쁜 다이얼로그
63 | implementation 'com.github.takusemba:spotlight:+' // 스포트라이트 UI
64 | implementation 'com.squareup.picasso:picasso:2.71828' // 쓰기편한 동적 이미지뷰
65 | // endregion
66 |
67 | implementation 'com.arthenica:mobile-ffmpeg-full:4.3.2' // FFMPEG
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_simpleuser.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
16 |
17 |
21 |
22 |
27 |
28 |
37 |
38 |
42 |
43 |
52 |
53 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Authenticate/AuthDataAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Authenticate
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context.LAYOUT_INFLATER_SERVICE
5 | import android.view.Gravity
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.BaseAdapter
10 | import android.widget.PopupMenu
11 | import com.google.gson.Gson
12 | import com.sasarinomari.tweeper.R
13 | import com.squareup.picasso.Picasso
14 | import kotlinx.android.synthetic.main.item_usertoken.view.*
15 |
16 |
17 | internal class AuthDataAdapter(private val users: ArrayList,
18 | private val ai: ActivityInterface) : BaseAdapter() {
19 |
20 | override fun getCount(): Int {
21 | return users.size
22 | }
23 |
24 | @SuppressLint("SetTextI18n", "SimpleDateFormat")
25 | override fun getView(position: Int, _convertView: View?, parent: ViewGroup): View {
26 | var convertView = _convertView
27 | val context = parent.context
28 |
29 | if (convertView == null) {
30 | val inflater = context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
31 | convertView = inflater.inflate(R.layout.item_usertoken, parent, false)
32 | }
33 |
34 | val authData = users[position]
35 |
36 | convertView!!
37 | if (authData.focused) convertView.image_focused.visibility = View.VISIBLE
38 | convertView.text_ScreenName.text = authData.user?.screenName
39 | convertView.text_Name.text = authData.user?.name
40 | Picasso.get()
41 | .load(authData.user?.profileImageUrl)
42 | .into(convertView.image_profilePicture)
43 |
44 | convertView.button_more.setOnClickListener { v ->
45 | val menu = PopupMenu(context, v)
46 | menu.setOnMenuItemClickListener { m ->
47 | when (m.itemId) {
48 | R.id.option_delete -> {
49 | ai.onDeleteUser(authData)
50 | }
51 | }
52 | true
53 | }
54 | menu.inflate(R.menu.user_token_item_menu)
55 | menu.gravity = Gravity.END
56 | menu.show()
57 | }
58 |
59 | convertView.setOnClickListener {
60 | ai.onSelectUser(authData)
61 | }
62 |
63 | return convertView
64 | }
65 |
66 |
67 | override fun getItemId(position: Int): Long {
68 | return position.toLong()
69 | }
70 |
71 | override fun getItem(position: Int): AuthData {
72 | return users[position]
73 | }
74 |
75 | fun getItemToJson(position: Int): String? {
76 | return Gson().toJson(getItem(position))
77 | }
78 |
79 | interface ActivityInterface {
80 | fun onSelectUser(authData: AuthData)
81 | fun onDeleteUser(authData: AuthData)
82 | }
83 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_logic_pair_edit.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
14 |
15 |
28 |
29 |
33 |
34 |
43 |
44 |
45 |
49 |
54 |
55 |
56 |
57 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Report/ReportListActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Report
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.view.View
7 | import android.widget.TextView
8 | import com.sasarinomari.tweeper.Authenticate.AuthData
9 | import com.sasarinomari.tweeper.Base.BaseActivity
10 | import com.sasarinomari.tweeper.R
11 | import com.sasarinomari.tweeper.Hetzer.HetzerReportActivity
12 | import kotlinx.android.synthetic.main.activity_report_list.*
13 | import java.text.SimpleDateFormat
14 | import java.util.*
15 |
16 | /**
17 | * 안씀!
18 | * 기술적인 문제는 없지만 이거 말고 ReportListFragment나 쓰셈 ㅋ
19 | * 사유: 못생김
20 | */
21 | /**
22 | * 트윗지기 서비스에서 생성된 리포트 조회용 공용 액티비티.
23 | * Parameters 전부가 인자로 와야 실행 가능.
24 | *
25 | * ReportActivityName은 아이템 클릭 시 redirect할 class 이름임.
26 | * Class::class.java.name과 같이 써서 인자로 넣을 수 있음.
27 | */
28 | @Suppress("DEPRECATION")
29 | @Deprecated("Use ReportListFragment")
30 | class ReportListActivity : BaseActivity() {
31 | enum class Parameters {
32 | Title, Description, NoReportDescription,
33 | ReportPrefix, ReportActivityName
34 | }
35 |
36 | private fun checkRequirements(): Boolean {
37 | for(parameter in Parameters.values())
38 | if(!intent.hasExtra(parameter.name)) return false
39 | return true
40 | }
41 |
42 | override fun onCreate(savedInstanceState: Bundle?) {
43 | super.onCreate(savedInstanceState)
44 | setContentView(R.layout.activity_report_list)
45 | if(!checkRequirements()) {
46 | finish(); return
47 | }
48 |
49 | text_title.text = intent.getStringExtra(Parameters.Title.name)
50 | text_description.text = intent.getStringExtra(Parameters.Description.name)
51 | text_noReport.text = intent.getStringExtra(Parameters.NoReportDescription.name)
52 |
53 | val reportPrefix = intent.getStringExtra(Parameters.ReportPrefix.name)!!
54 | val userId = AuthData.Recorder(this).getFocusedUser()!!.user!!.id
55 | val reports = ReportInterface(userId, reportPrefix).getReportsWithName(this)
56 | if (reports.isEmpty()) {
57 | layout_noReport.visibility = View.VISIBLE
58 | listView.visibility = View.GONE
59 | } else {
60 | val adapter = object : com.sasarinomari.tweeper.View.DefaultListItem(reports) {
61 | @SuppressLint("SimpleDateFormat")
62 | override fun drawItem(item: Any, title: TextView, description: TextView) {
63 | item as Pair<*, *>
64 | title.text = item.first.toString()
65 | description.text = SimpleDateFormat(getString(R.string.Format_DateTime)).format(item.second as Date)
66 | }
67 | }
68 | listView.adapter = adapter
69 | listView.setOnItemClickListener { _, _, index, _ ->
70 | val reportIndex = (adapter.getItem(index) as Pair<*, *>).first.toString().removePrefix(reportPrefix).toInt()
71 | val intent = Intent(this@ReportListActivity,
72 | Class.forName(intent.getStringExtra(Parameters.ReportActivityName.name)!!))
73 | intent.putExtra(HetzerReportActivity.Parameters.ReportId.name, reportIndex)
74 | startActivity(intent)
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_block_clear.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
21 |
22 |
29 |
30 |
38 |
39 |
40 |
41 |
48 |
49 |
57 |
58 |
59 |
60 |
64 |
65 |
80 |
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.os.Bundle
4 | import android.content.Intent
5 | import com.sasarinomari.tweeper.Authenticate.AuthData
6 | import com.sasarinomari.tweeper.Authenticate.TokenManagementActivity
7 | import com.sasarinomari.tweeper.Base.BaseActivity
8 | import com.sasarinomari.tweeper.SimplizatedClass.User
9 | import java.lang.Exception
10 |
11 |
12 | class MainActivity : BaseActivity() {
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | setContentView(R.layout.activity_main)
16 | NotificationChannels().declaration(this)
17 | TwitterAdapter.TwitterInterface.setOAuthConsumer(this)
18 | RewardedAdAdapter.load(this)
19 |
20 | when (val loggedUser = AuthData.Recorder(this).getFocusedUser()) {
21 | null -> doAuth()
22 | else -> lookupSelf(loggedUser)
23 | }
24 |
25 | checkNotiPremission()
26 | }
27 |
28 | private fun lookupSelf(loggedUser: AuthData) {
29 | Thread {
30 | try {
31 | TwitterAdapter().initialize(this, loggedUser.token!!).getMe(object : TwitterAdapter.FetchObjectInterface {
32 | override fun onStart() {}
33 |
34 | override fun onFinished(obj: Any) {
35 | val me = obj as twitter4j.User
36 | loggedUser.user = User(me)
37 | AuthData.Recorder(this@MainActivity).setFocusedUser(loggedUser)
38 | openDashboard()
39 | finish()
40 | }
41 |
42 | override fun onRateLimit() {
43 | this@MainActivity.onRateLimit("getMe") { finish() }
44 | }
45 |
46 | override fun onUncaughtError() {
47 | runOnUiThread {
48 | da.error("로그인에 실패했습니다.", "계정 선택 화면으로 이동합니다.") {
49 | doAuth()
50 | }.show()
51 | }
52 | }
53 |
54 | override fun onNetworkError(retry: () -> Unit) {
55 | this@MainActivity.onNetworkError {
56 | lookupSelf(loggedUser)
57 | }
58 | }
59 | })
60 | } catch (e: Exception) {
61 | doAuth()
62 | }
63 | }.start()
64 | }
65 |
66 | private fun checkNotiPremission() {
67 | // TODO
68 | // 백그라운드 스레드 실행을 위해 알림을 띄울 수 있는 권한이 있는지 검사.
69 | // 권한이 없다면 안내 문구와 함께 설정 창으로 연결, 또는 앱 종료료
70 | }
71 |
72 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
73 | when (requestCode) {
74 | 0 -> {
75 | if(resultCode == RESULT_OK) {
76 | openDashboard()
77 | }
78 | finish()
79 | }
80 | else -> super.onActivityResult(requestCode, resultCode, data)
81 | }
82 | }
83 |
84 | private fun openDashboard() {
85 | startActivity(Intent(this, DashboardActivity::class.java))
86 | }
87 |
88 | private fun doAuth() {
89 | startActivityForResult(Intent(this, TokenManagementActivity::class.java), 0)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Authenticate/AuthData.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Authenticate
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import com.google.gson.Gson
6 | import com.google.gson.reflect.TypeToken
7 | import com.sasarinomari.tweeper.SimplizatedClass.User
8 | import twitter4j.auth.AccessToken
9 | import java.util.*
10 |
11 | class AuthData {
12 | var token: AccessToken? = null
13 | var focused: Boolean = false
14 | var user: User? = null
15 | var lastLogin: Date? = null
16 |
17 | override operator fun equals(other: Any?): Boolean {
18 | return if (other is AuthData) this.user?.id == other.user?.id else super.equals(other)
19 | }
20 |
21 | override fun hashCode(): Int {
22 | return token?.token.hashCode()
23 | }
24 |
25 | internal class Recorder(private val context: Context) {
26 | private var prefId = "record"
27 | private var key = "auth"
28 |
29 | @SuppressLint("CommitPrefEdits")
30 | fun addUser(report: AuthData) {
31 | val prefs = context.getSharedPreferences(prefId, Context.MODE_PRIVATE).edit()
32 | val authData = getUsers()
33 | authData.add(0, report)
34 | val json = Gson().toJson(authData)
35 | prefs.putString(key, json)
36 | prefs.apply()
37 | }
38 |
39 | fun hasUser(authData: AuthData): Boolean {
40 | return getUsers().contains(authData)
41 | }
42 |
43 | private fun saveUsers(authData: ArrayList) {
44 | val prefs = context.getSharedPreferences(prefId, Context.MODE_PRIVATE).edit()
45 | val json = Gson().toJson(authData)
46 | prefs.putString(key, json)
47 | prefs.apply()
48 | }
49 |
50 | fun getUsers(): ArrayList {
51 | val prefs = context.getSharedPreferences(prefId, Context.MODE_PRIVATE)
52 | val json = prefs.getString(key, null) ?: return ArrayList()
53 | val type = object : TypeToken>() {}.type
54 | return Gson().fromJson(json, type)
55 | }
56 |
57 | fun getFocusedUser(): AuthData? {
58 | val users = getUsers()
59 | if (users.isEmpty()) return null
60 | return try {
61 | users.first { i -> i.focused }
62 | } catch (e: Exception) {
63 | setFocusedUser(users[0])
64 | getFocusedUser()
65 | }
66 | }
67 |
68 | fun setFocusedUser(authData: AuthData) {
69 | val users = getUsers()
70 | for (user in users) {
71 | user.focused = if(user == authData) {
72 | user.user = authData.user
73 | true
74 | } else false
75 | }
76 | saveUsers(users)
77 | }
78 |
79 | fun deleteUser(authData: AuthData) {
80 | val users = getUsers()
81 | users.remove(authData)
82 | if (authData.focused && users.isNotEmpty()) {
83 | users[0].focused = true
84 | }
85 | saveUsers(users)
86 | }
87 |
88 | fun getUser(id: Long): AuthData? {
89 | val users = getUsers()
90 | if(users.isEmpty()) return null
91 | return users.first {i -> i.user?.id == id}
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_token_management.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
21 |
22 |
29 |
30 |
38 |
39 |
40 |
41 |
42 |
58 |
59 |
63 |
64 |
73 |
74 |
75 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Permission/PermissionHelper.java:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Permission;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.pm.PackageManager;
6 | import android.os.Handler;
7 | import android.util.Log;
8 | import com.sasarinomari.tweeper.R;
9 |
10 | public class PermissionHelper {
11 | private static final String TAG = "PermissionHelper";
12 | private static Runnable callback;
13 |
14 | private static final int PERMISSION_REQUEST_CODE = 1000;
15 |
16 | public static void activatePermission(final Activity context, String[] permissions, Runnable callback) {
17 | PermissionHelper.callback = callback;
18 |
19 | int permissionResult = new PermissionRequester.Builder(context)
20 | .setTitle(context.getString(R.string.PermissionRequestTitle))
21 | .setPositiveButtonName(context.getString(R.string.Yes))
22 | .setNegativeButtonName(context.getString(R.string.No))
23 | .create()
24 | .request(permissions, PERMISSION_REQUEST_CODE,
25 | activity -> Log.d(TAG, "Permission denied by user."));
26 |
27 | if (permissionResult == PermissionRequester.ALREADY_GRANTED) {
28 | Log.d(TAG, "Permission already granted.");
29 | if (checkSelfAllPermissions(context, permissions)) {
30 | callback.run();
31 | }
32 | } else if (permissionResult == PermissionRequester.NOT_SUPPORTED_VERSION) {
33 | Log.d(TAG, "No supported version");
34 | callback.run();
35 | } else if (permissionResult == PermissionRequester.REQUEST_PERMISSION) {
36 | Log.d(TAG, "Requested permission");
37 | }
38 |
39 | }
40 |
41 | private static boolean checkSelfAllPermissions(Context context, String[] permissions) {
42 | for (String permission : permissions) {
43 | // Activity Compat 기반 권한 체크
44 | //if ( ActivityCompat.checkSelfPermission( context, permission ) == PackageManager.PERMISSION_DENIED )
45 |
46 | // Context 기반 권한 체크
47 | if (context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_DENIED) {
48 | return false;
49 | }
50 | }
51 | return true;
52 | }
53 |
54 | public static void onRequestPermissionsResult(Context context, String[] permissions, int requestCode, int[] grantResults, Runnable deniedCallback) {
55 | if (requestCode == PERMISSION_REQUEST_CODE) {
56 | if (grantResults.length > 0) {
57 | boolean haveAllPermissionsGranted = true;
58 |
59 | for (int grantResult : grantResults) {
60 | if (grantResult == PackageManager.PERMISSION_DENIED) {
61 | haveAllPermissionsGranted = false;
62 | }
63 | }
64 | if (haveAllPermissionsGranted) {
65 | Log.d(TAG, "Permission Granted by user.");
66 |
67 | if (checkSelfAllPermissions(context, permissions)) {
68 | new Handler().post(PermissionHelper.callback);
69 | }
70 | } else {
71 | Log.d(TAG, "Permission denied by user.");
72 | deniedCallback.run();
73 | }
74 | } else {
75 | Log.d(TAG, "No result");
76 | }
77 | }
78 | }
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/DefaultRecycleAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.TextView
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.google.gson.Gson
9 | import kotlinx.android.synthetic.main.item_default.view.*
10 |
11 | @Deprecated("더 좋은 클래스 RecyclerInjector를 준비해왔어요 ㅎㅎ..")
12 | abstract class DefaultRecycleAdapter(private val items: List<*>,
13 | private val header: Int = 0,
14 | private val footer: Int = 0) : RecyclerView.Adapter() {
15 |
16 | enum class ViewType {
17 | Header, ListData, Footer
18 | }
19 |
20 | class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
21 | class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
22 | class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
23 |
24 | override fun getItemViewType(position: Int): Int {
25 | return when (position) {
26 | 0-> ViewType.Header.ordinal
27 | items.size+1 -> ViewType.Footer.ordinal
28 | else -> ViewType.ListData.ordinal
29 | }
30 | }
31 |
32 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
33 | val view: View
34 | return when(viewType) {
35 | ViewType.Header.ordinal -> {
36 | view = LayoutInflater.from(parent.context).inflate(header, parent, false)
37 | HeaderViewHolder(view)
38 | }
39 | ViewType.Footer.ordinal -> {
40 | view = LayoutInflater.from(parent.context).inflate(footer, parent, false)
41 | FooterViewHolder(view)
42 | }
43 | ViewType.ListData.ordinal -> {
44 | view = LayoutInflater.from(parent.context).inflate(R.layout.item_default, parent, false)
45 | ListViewHolder(view)
46 | }
47 | else -> throw Exception("ViewType Enum Out of Range.")
48 | }
49 | }
50 |
51 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
52 | when (holder) {
53 | is HeaderViewHolder -> drawHeader(holder.itemView)
54 | is FooterViewHolder -> drawFooter(holder.itemView)
55 | is ListViewHolder -> {
56 | val item = items[position - 1]!!
57 | drawListItem(item, holder.itemView.defaultitem_title, holder.itemView.defaultitem_description)
58 | holder.itemView.setOnClickListener{ onClickListItem(item) }
59 | }
60 | }
61 | }
62 |
63 | open fun drawHeader(view: View) { }
64 | open fun drawFooter(view: View) { }
65 | abstract fun drawListItem(item: Any, title: TextView, description: TextView)
66 | abstract fun onClickListItem(item: Any)
67 |
68 | // region List 관련 함수
69 | private fun existsHeader(): Int {
70 | return if(header == 0) 0 else 1
71 | }
72 | private fun existsFooter(): Int {
73 | return if(footer == 0) 0 else 1
74 | }
75 | override fun getItemCount(): Int {
76 | return items.size + existsHeader() + existsFooter()
77 | }
78 | override fun getItemId(position: Int): Long {
79 | return position.toLong() +existsHeader()
80 | }
81 | fun getItem(position: Int): Any? {
82 | return items[position+existsHeader()]
83 | }
84 | fun getItemToJson(position: Int): String? {
85 | return Gson().toJson(getItem(position))
86 | }
87 | // endregion
88 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/RewardedAdAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.util.Log
6 | import com.google.android.gms.ads.AdRequest
7 | import com.google.android.gms.ads.rewarded.RewardedAd
8 | import com.google.android.gms.ads.rewarded.RewardedAdCallback
9 | import com.google.android.gms.ads.rewarded.RewardedAdLoadCallback
10 | import com.sasarinomari.tweeper.Billing.AdRemover
11 | import java.util.*
12 |
13 | class RewardedAdAdapter {
14 | interface RewardInterface {
15 | fun onFinished()
16 | }
17 |
18 | companion object {
19 | private const val LOG_TAG = "RewardedAdAdapter"
20 | private var rewardedAd: RewardedAd? = null
21 |
22 | fun load(context: Context) {
23 | if(AdRemover(context).isAdRemoved()) return
24 |
25 | rewardedAd = RewardedAd(
26 | context, context.getString(
27 | if (BuildConfig.DEBUG) R.string.reward_ad_unit_id_for_test
28 | else R.string.reward_ad_unit_id
29 | )
30 | )
31 |
32 | val adLoadCallback = object : RewardedAdLoadCallback() {
33 | override fun onRewardedAdLoaded() {
34 | Log.i(LOG_TAG, "onRewardedAdLoaded")
35 | }
36 |
37 | override fun onRewardedAdFailedToLoad(errorCode: Int) {
38 | Log.i(LOG_TAG, "onRewardedAdFailedToLoad $errorCode")
39 | FirebaseLogger(context).log("onRewardedAdFailedToLoad",
40 | Pair("ErrorCode", errorCode.toString()))
41 | }
42 | }
43 | rewardedAd!!.loadAd(AdRequest.Builder().build(), adLoadCallback)
44 | }
45 |
46 | private val random = Random()
47 | /**
48 | * 양심에 따라 광고가 낮은 확률로 나오도록 설정
49 | */
50 | private fun luckey(): Boolean {
51 | val r = random.nextInt(100)
52 | return r > 20
53 | }
54 |
55 | fun show(activity: Activity, ri: RewardInterface) {
56 | when {
57 | AdRemover(activity).isAdRemoved() -> {
58 | ri.onFinished()
59 | }
60 | BuildConfig.DEBUG ->{
61 | ri.onFinished()
62 | }
63 | luckey() -> {
64 | ri.onFinished()
65 | }
66 | rewardedAd != null && rewardedAd!!.isLoaded -> {
67 | val adCallback = object : RewardedAdCallback() {
68 | var isCompleted: Boolean = false
69 | override fun onRewardedAdOpened() {
70 | Log.i(LOG_TAG, "onRewardedAdOpened")
71 | }
72 |
73 | override fun onRewardedAdClosed() {
74 | Log.i(LOG_TAG, "onRewardedAdClosed")
75 | if(isCompleted) ri.onFinished()
76 | load(activity)
77 | }
78 |
79 | override fun onUserEarnedReward(reward: com.google.android.gms.ads.rewarded.RewardItem) {
80 | Log.i(LOG_TAG, "onUserEarnedReward ${reward.type}")
81 | isCompleted = true
82 | }
83 |
84 | override fun onRewardedAdFailedToShow(errorCode: Int) {
85 | Log.i(LOG_TAG, "onRewardedAdFailedToShow $errorCode")
86 | FirebaseLogger(activity).log("onRewardedAdFailedToShow",
87 | Pair("ErrorCode", errorCode.toString()))
88 | }
89 | }
90 | rewardedAd!!.show(activity, adCallback)
91 | }
92 | else -> {
93 | Log.d(LOG_TAG, "The rewarded ad wasn't loaded yet.")
94 | ri.onFinished()
95 | }
96 | }
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Analytics/AnalyticsNotificationReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Analytics
2 |
3 | import android.app.AlarmManager
4 | import android.app.PendingIntent
5 | import android.content.BroadcastReceiver
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.os.Build
9 | import android.os.Bundle
10 | import android.util.Log
11 | import com.google.gson.Gson
12 | import com.sasarinomari.tweeper.Authenticate.AuthData
13 | import com.sasarinomari.tweeper.BuildConfig
14 | import com.sasarinomari.tweeper.Tweeper
15 | import java.util.*
16 |
17 | class AnalyticsNotificationReceiver : BroadcastReceiver() {
18 | enum class Parameters {
19 | Bundle
20 | }
21 |
22 | override fun onReceive(context: Context, intent: Intent) {
23 | val b = intent.getBundleExtra(Parameters.Bundle.name)
24 | val userJson = b?.getString(AnalyticsService.Parameters.User.name)
25 | if (userJson == null) {
26 | Log.e("AnalyticsNotificationReceiver", "리시버가 호출되었지만 User가 Null입니다.")
27 | return
28 | }
29 | val i = Intent(context, AnalyticsService::class.java)
30 | i.putExtra(AnalyticsService.Parameters.User.name, userJson)
31 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
32 | context.startForegroundService(i)
33 | } else {
34 | context.startService(i)
35 | }
36 | }
37 |
38 | companion object {
39 | fun isApplied(context: Context) : Boolean {
40 | val pref = context.getSharedPreferences("AnalyticsNotificationReceiver", Context.MODE_PRIVATE)
41 | return pref.getBoolean("scheduleEnabled", false)
42 | }
43 |
44 | fun apply(context: Context, checked: Boolean) : Boolean {
45 | var checked = checked
46 | val user = AuthData.Recorder(context).getFocusedUser() ?: return false
47 | /**
48 | * getFocusedUser에서 null 반환하는 경우가 들어와서 예외처리함. 대체 왜지?
49 | */
50 |
51 | val intent = Intent(context, AnalyticsNotificationReceiver::class.java)
52 | val bundle = Bundle()
53 | bundle.putString(
54 | AnalyticsService.Parameters.User.name,
55 | Gson().toJson(user)
56 | )
57 | intent.putExtra(AnalyticsNotificationReceiver.Parameters.Bundle.name, bundle)
58 | val pendingIntent = PendingIntent.getBroadcast(
59 | context, Tweeper.RequestCodes.ScheduledAnalytics.ordinal, intent, PendingIntent.FLAG_UPDATE_CURRENT
60 | )
61 |
62 | // 체크 해제된 경우 알람 해제
63 | val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
64 | if (pendingIntent != null) alarmManager.cancel(pendingIntent)
65 | if(BuildConfig.DEBUG) Log.d("Schedule", "알람을 해제했습니다.")
66 |
67 | if (checked) {
68 | val calendar: Calendar = Calendar.getInstance().apply {
69 | set(Calendar.HOUR_OF_DAY, 21)
70 | set(Calendar.MINUTE, 0)
71 | set(Calendar.SECOND, 0)
72 | }
73 | /**
74 | * 오늘 이미 9시가 지났다면 내일 아홉시에 실행하기
75 | */
76 | if(calendar.after(Calendar.getInstance())) {
77 | calendar.add(Calendar.DAY_OF_YEAR, 1)
78 | }
79 |
80 | alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, 1000 * 60 * 60 * 24, pendingIntent)
81 | if(BuildConfig.DEBUG) Log.e("Schedule", "알람을 등록했습니다.")
82 | } else {
83 | checked = false
84 |
85 | // 아마 무한재귀 걸릴 듯? 나중에 짬나면 처리하던가 하자
86 | // checkbox_setScheduled.isChecked = false
87 | }
88 |
89 | val edit = context.getSharedPreferences("AnalyticsNotificationReceiver", Context.MODE_PRIVATE).edit()
90 | edit.putBoolean("scheduleEnabled", checked)
91 | edit.apply()
92 |
93 | return true
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/MediaDownload/DownloadReceiver.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.MediaDownload
2 |
3 | import android.app.DownloadManager
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.util.Log
8 | import android.widget.Toast
9 | import com.google.gson.Gson
10 | import com.google.gson.reflect.TypeToken
11 | import com.sasarinomari.tweeper.R
12 | import java.io.File
13 | import java.io.FileNotFoundException
14 |
15 | class DownloadReceiver : BroadcastReceiver() {
16 | internal companion object GifList {
17 | private val key = "gifIds"
18 |
19 | fun add(context: Context, gifIdAndString: Pair) {
20 | val list = fetch(context)
21 | val prefs = context.getSharedPreferences(this::class.java.name, Context.MODE_PRIVATE).edit()
22 | list.add(gifIdAndString)
23 | prefs.putString(key, Gson().toJson(list))
24 | prefs.apply()
25 | }
26 |
27 | fun fetch(context: Context) : ArrayList> {
28 | val prefs = context.getSharedPreferences(this::class.java.name, Context.MODE_PRIVATE)
29 | // Custrom Pair 읽어오고 gif면 변환 후 리스트에서 삭제
30 | val json = prefs.getString(key, null)?: return ArrayList()
31 | return Gson().fromJson(json, object : TypeToken>>() {}.type)
32 | }
33 |
34 | fun drop(context: Context, gifIdAndString: Pair) {
35 | val list = fetch(context)
36 | val prefs = context.getSharedPreferences("fr${this::class.java.name}", Context.MODE_PRIVATE).edit()
37 | list.remove(gifIdAndString)
38 | prefs.putString(key, Gson().toJson(list))
39 | prefs.apply()
40 | }
41 | }
42 |
43 | override fun onReceive(context: Context, intent: Intent) {
44 | return
45 | /**
46 | * Download Manager 사용 시 GIF 변환을 여기서 처리해야 할 것 같은데...
47 | * 일단 제대로 동작 안함. 수정 요
48 | */
49 | val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
50 | if (DownloadManager.ACTION_DOWNLOAD_COMPLETE == intent.action) {
51 | val gifList = fetch(context)
52 | for(it in gifList)
53 | if(it.first == id) {
54 | val query: DownloadManager.Query = DownloadManager.Query()
55 | query.setFilterById(id)
56 | val cursor = (context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager).query(query)
57 | if (!cursor.moveToFirst()) {
58 | return
59 | }
60 |
61 | val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
62 | val status = cursor.getInt(columnIndex)
63 | if (status == DownloadManager.STATUS_SUCCESSFUL) {
64 | val file = File(it.second)
65 | if(!file.exists()) {
66 | throw FileNotFoundException()
67 | }
68 | else {
69 | // sendNotification(getString(R.string.MediaDownloader), getString(R.string.Converting), silent = true)
70 | val newPath = MediaDownloadService.convertWithFFMPEG(file)
71 | Log.i("GIF", "GIF 내보내기 완료")
72 | /*
73 | val i = getOpenFileIntent(newPath)
74 | sendNotification(getString(R.string.MediaDownloader), getString(R.string.DownloadCompleted),
75 | silent = false,
76 | cancelable = true,
77 | redirect = i,
78 | id = NotificationId + 1
79 | )
80 | finish()
81 | */
82 | }
83 | }
84 | }
85 | } else if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(intent.action)) {
86 | Toast.makeText(context, "Notification clicked", Toast.LENGTH_SHORT).show()
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/policy.txt:
--------------------------------------------------------------------------------
1 | < 트윗지기 >('https://github.com/트윗지기/Tweeper'이하 '트윗지기')은(는) 「개인정보 보호법」 제30조에 따라 정보주체의 개인정보를 보호하고 이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여 다음과 같이 개인정보 처리방침을 수립·공개합니다.
2 |
3 | ○ 이 개인정보처리방침은 2023년 7월 14부터 적용됩니다.
4 |
5 |
6 | 제1조(개인정보의 처리 목적)
7 |
8 | < 트윗지기 >('https://github.com/트윗지기/Tweeper'이하 '트윗지기')은(는) 다음의 목적을 위하여 개인정보를 처리합니다. 처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며 이용 목적이 변경되는 경우에는 「개인정보 보호법」 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.
9 |
10 | 1. 재화 또는 서비스 제공
11 |
12 | 서비스 제공을 목적으로 개인정보를 처리합니다.
13 |
14 |
15 |
16 |
17 | 제2조(개인정보의 처리 및 보유 기간)
18 |
19 | ① < 트윗지기 >은(는) 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에 동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.
20 |
21 | ② 각각의 개인정보 처리 및 보유 기간은 다음과 같습니다.
22 |
23 | 1.<재화 또는 서비스 제공>
24 | <재화 또는 서비스 제공>와 관련한 개인정보는 수집.이용에 관한 동의일로부터<지체없이 파기>까지 위 이용목적을 위하여 보유.이용됩니다.
25 | 보유근거 : -
26 | 관련법령 :
27 | 예외사유 :
28 |
29 |
30 | 제3조(처리하는 개인정보의 항목)
31 |
32 | ① < 트윗지기 >은(는) 다음의 개인정보 항목을 처리하고 있습니다.
33 |
34 | 1< 재화 또는 서비스 제공 >
35 | 필수항목 : 트위터 API 토큰
36 | 선택항목 :
37 |
38 |
39 | 제4조(개인정보의 파기절차 및 파기방법)
40 |
41 |
42 | ① < 트윗지기 > 은(는) 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체없이 해당 개인정보를 파기합니다.
43 |
44 | ② 정보주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는, 해당 개인정보를 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.
45 | 1. 법령 근거 :
46 | 2. 보존하는 개인정보 항목 : 계좌정보, 거래날짜
47 |
48 | ③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.
49 | 1. 파기절차
50 | < 트윗지기 > 은(는) 파기 사유가 발생한 개인정보를 선정하고, < 트윗지기 > 의 개인정보 보호책임자의 승인을 받아 개인정보를 파기합니다.
51 |
52 |
53 |
54 | 제5조(정보주체와 법정대리인의 권리·의무 및 그 행사방법에 관한 사항)
55 |
56 |
57 |
58 | ① 정보주체는 트윗지기에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다.
59 |
60 | ② 제1항에 따른 권리 행사는트윗지기에 대해 「개인정보 보호법」 시행령 제41조제1항에 따라 서면, 전자우편, 모사전송(FAX) 등을 통하여 하실 수 있으며 트윗지기은(는) 이에 대해 지체 없이 조치하겠습니다.
61 |
62 | ③ 제1항에 따른 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다.이 경우 “개인정보 처리 방법에 관한 고시(제2020-7호)” 별지 제11호 서식에 따른 위임장을 제출하셔야 합니다.
63 |
64 | ④ 개인정보 열람 및 처리정지 요구는 「개인정보 보호법」 제35조 제4항, 제37조 제2항에 의하여 정보주체의 권리가 제한 될 수 있습니다.
65 |
66 | ⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.
67 |
68 | ⑥ 트윗지기은(는) 정보주체 권리에 따른 열람의 요구, 정정·삭제의 요구, 처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.
69 |
70 |
71 |
72 | 제6조(개인정보의 안전성 확보조치에 관한 사항)
73 |
74 | < 트윗지기 >은(는) 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.
75 |
76 | 1. 개인정보에 대한 접근 제한
77 | 개인정보를 처리하는 데이터베이스시스템에 대한 접근권한의 부여,변경,말소를 통하여 개인정보에 대한 접근통제를 위하여 필요한 조치를 하고 있으며 침입차단시스템을 이용하여 외부로부터의 무단 접근을 통제하고 있습니다.
78 |
79 |
80 |
81 |
82 | 제7조(개인정보를 자동으로 수집하는 장치의 설치·운영 및 그 거부에 관한 사항)
83 |
84 |
85 |
86 | 트윗지기 은(는) 정보주체의 이용정보를 저장하고 수시로 불러오는 ‘쿠키(cookie)’를 사용하지 않습니다.
87 |
88 | 제8조 (개인정보 보호책임자에 관한 사항)
89 |
90 | ① 트윗지기 은(는) 개인정보 처리에 관한 업무를 총괄해서 책임지고, 개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.
91 |
92 | ▶ 개인정보 보호책임자
93 | 성명 :Sasarino MARi
94 | 직책 :본인
95 | 직급 :본인
96 | 연락처 :develop.mari@gmail.com,
97 | ※ 개인정보 보호 담당부서로 연결됩니다.
98 |
99 | ② 정보주체께서는 트윗지기 의 서비스(또는 사업)을 이용하시면서 발생한 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다. 트윗지기 은(는) 정보주체의 문의에 대해 지체 없이 답변 및 처리해드릴 것입니다.
100 |
101 | 제9조(개인정보의 열람청구를 접수·처리하는 부서)
102 | 정보주체는 「개인정보 보호법」 제35조에 따른 개인정보의 열람 청구를 아래의 부서에 할 수 있습니다.
103 | < 트윗지기 >은(는) 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.
104 |
105 | ▶ 개인정보 열람청구 접수·처리 부서
106 | 부서명 :
107 | 담당자 :
108 | 연락처 : , ,
109 |
110 |
111 | 제10조(정보주체의 권익침해에 대한 구제방법)
112 |
113 |
114 |
115 | 정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회, 한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고, 상담에 대하여는 아래의 기관에 문의하시기 바랍니다.
116 |
117 | 1. 개인정보분쟁조정위원회 : (국번없이) 1833-6972 (www.kopico.go.kr)
118 | 2. 개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)
119 | 3. 대검찰청 : (국번없이) 1301 (www.spo.go.kr)
120 | 4. 경찰청 : (국번없이) 182 (ecrm.cyber.go.kr)
121 |
122 | 「개인정보보호법」제35조(개인정보의 열람), 제36조(개인정보의 정정·삭제), 제37조(개인정보의 처리정지 등)의 규정에 의한 요구에 대 하여 공공기관의 장이 행한 처분 또는 부작위로 인하여 권리 또는 이익의 침해를 받은 자는 행정심판법이 정하는 바에 따라 행정심판을 청구할 수 있습니다.
123 |
124 | ※ 행정심판에 대해 자세한 사항은 중앙행정심판위원회(www.simpan.go.kr) 홈페이지를 참고하시기 바랍니다.
125 |
126 | 제11조(개인정보 처리방침 변경)
127 |
128 |
129 | ① 이 개인정보처리방침은 2023년 7월 14부터 적용됩니다.
130 |
131 | ② 이전의 개인정보 처리방침은 아래에서 확인하실 수 있습니다.
132 |
133 | 예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)
134 |
135 | 예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)
136 |
137 | 예시 ) - 20XX. X. X ~ 20XX. X. X 적용 (클릭)
138 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Base
2 |
3 | import android.content.Context
4 | import android.content.IntentFilter
5 | import android.content.SharedPreferences
6 | import android.os.Bundle
7 | import android.view.inputmethod.InputMethodManager
8 | import android.widget.Toast
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
11 | import com.google.gson.Gson
12 | import com.sasarinomari.tweeper.DialogAdapter
13 | import com.sasarinomari.tweeper.FirebaseLogger
14 | import com.sasarinomari.tweeper.R
15 |
16 | abstract class BaseActivity : AppCompatActivity() {
17 | fun hideKeyboard() {
18 | val windowToken = currentFocus?.windowToken
19 | val imm = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
20 | imm.hideSoftInputFromWindow(windowToken, 0)
21 | }
22 |
23 | private val FINISH_INTERVAL_TIME: Long = 2000
24 | private var backPressedTime: Long = 0
25 | protected lateinit var da: DialogAdapter
26 | private lateinit var activityRefrashReceiver: ActivityRefrashReceiver
27 | protected lateinit var fbLog: FirebaseLogger
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | da = DialogAdapter(this)
32 | activityRefrashReceiver = ActivityRefrashReceiver(this)
33 | fbLog = FirebaseLogger(this)
34 | }
35 |
36 | fun backPressJail(warningText: String): Boolean {
37 | val tempTime = System.currentTimeMillis()
38 | val intervalTime = tempTime - backPressedTime
39 |
40 | return when (intervalTime) {
41 | in 0..FINISH_INTERVAL_TIME -> false
42 | else -> {
43 | Toast.makeText(this, warningText, Toast.LENGTH_SHORT).show()
44 | backPressedTime = tempTime
45 | true
46 | }
47 | }
48 | }
49 |
50 | open fun onFinish() { }
51 | override fun finish() {
52 | onFinish()
53 | super.finish()
54 | }
55 |
56 | override fun onResume() {
57 | super.onResume()
58 | LocalBroadcastManager.getInstance(this).registerReceiver( activityRefrashReceiver,
59 | IntentFilter(ActivityRefrashReceiver.eventName))
60 | }
61 |
62 | override fun onPause() {
63 | super.onPause()
64 | LocalBroadcastManager.getInstance(this).unregisterReceiver( activityRefrashReceiver)
65 | }
66 |
67 | //region FirstRun
68 | val activityPreference : SharedPreferences by lazy {
69 | getSharedPreferences("fr${this::class.java.name}", Context.MODE_PRIVATE)
70 | }
71 | protected fun isFirstRunThisActivity() : Boolean {
72 | val flag = activityPreference.getInt("flag", 0)
73 | return flag == 0
74 | }
75 | protected fun setNotFirstrun(){
76 | val prefs = activityPreference.edit()
77 | prefs.putInt("flag", 1)
78 | prefs.apply()
79 | }
80 | //endregion
81 |
82 |
83 | fun onRateLimit(apiName: String, callback: () -> Unit = { }) {
84 | runOnUiThread {
85 | da.error(getString(R.string.Error), getString(R.string.RateLimitError, apiName)) {
86 | callback()
87 | }.show()
88 | }
89 | }
90 |
91 | fun onUncaughtError() {
92 | runOnUiThread {
93 | da.error(getString(R.string.Error), getString(R.string.UncaughtError)) {
94 | finish()
95 | }.show()
96 | }
97 | }
98 |
99 | fun onNoRequirement() {
100 | runOnUiThread {
101 | da.error(getString(R.string.Error), getString(R.string.Error_NoParameter)) {
102 | finish()
103 | }.show()
104 | }
105 | }
106 |
107 | fun onNetworkError(retry: () -> Unit) {
108 | runOnUiThread {
109 | val d = da.error(getString(R.string.Error), getString(R.string.NetworkError))
110 | .setConfirmText(getString(R.string.Yes))
111 | .setCancelText(getString(R.string.No))
112 | d.setConfirmClickListener {
113 | it.dismissWithAnimation()
114 | retry()
115 | }
116 | d.setCancelClickListener {
117 | it.dismiss()
118 | finish()
119 | }
120 | d.show()
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/ChainBlock/BlockClearService.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.ChainBlock
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import com.google.gson.Gson
6 | import com.sasarinomari.tweeper.Authenticate.AuthData
7 | import com.sasarinomari.tweeper.Base.BaseService
8 | import com.sasarinomari.tweeper.R
9 | import com.sasarinomari.tweeper.TwitterAdapter
10 |
11 | class BlockClearService : BaseService() {
12 | enum class Parameters {
13 | User
14 | }
15 |
16 | companion object {
17 | fun checkServiceRunning(context: Context) = BaseService.checkServiceRunning(context, BlockClearService::class.java.name)
18 | }
19 |
20 | lateinit var strServiceName: String
21 | lateinit var strRateLimitWaiting: String
22 |
23 | private val twitterAdapter = TwitterAdapter()
24 |
25 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
26 | if (super.onStartCommand(intent!!, flags, startId) == START_NOT_STICKY) return START_NOT_STICKY
27 | strServiceName = getString(R.string.BlockClear)
28 | strRateLimitWaiting = getString(R.string.RateLimitWaiting)
29 |
30 | val user = Gson().fromJson(intent.getStringExtra(Parameters.User.name), AuthData::class.java)
31 | twitterAdapter.initialize(this, user.token!!)
32 |
33 | startForeground(NotificationId, createNotification(getString(R.string.app_name), "Initializing...", false))
34 |
35 | runOnManagedThread {
36 | twitterAdapter.getBlockedUsers(object: TwitterAdapter.FetchListInterface {
37 | override fun onStart() { }
38 |
39 | override fun onFinished(list: ArrayList<*>) {
40 | twitterAdapter.unblockUsers(list as ArrayList, object: TwitterAdapter.IterableInterface {
41 | override fun onStart() { }
42 |
43 | override fun onFinished() {
44 | // 알림 송출
45 | sendNotification(
46 | strServiceName,
47 | getString(R.string.BlockCleanDone, list.count()),
48 | silent = false,
49 | cancelable = true,
50 | id = NotificationId + 1
51 | )
52 |
53 | finish()
54 | }
55 |
56 | override fun onIterate(listIndex: Int) {
57 | restrainedNotification(
58 | strServiceName,
59 | getString(R.string.Unblocking, listIndex, list.size)
60 | ) // 초기값이 0이라 이거 가능
61 | }
62 |
63 | override fun onRateLimit(listIndex: Int) {
64 | sendNotification(
65 | "$strServiceName $strRateLimitWaiting",
66 | getString(R.string.Unblocking, listIndex, list.size)
67 | )
68 | }
69 |
70 | override fun onUncaughtError() {
71 | this@BlockClearService.onUncaughtError(strServiceName)
72 | }
73 |
74 | override fun onNetworkError(retrySelf: ()->Unit) {
75 | this@BlockClearService.onNetworkError(strServiceName, { retrySelf() })
76 | }
77 | })
78 | }
79 |
80 | override fun onFetch(listSize: Int) {
81 | restrainedNotification(strServiceName, getString(R.string.FetchingUser, listSize))
82 | }
83 |
84 | override fun onRateLimit(listSize: Int) {
85 | sendNotification(
86 | "$strServiceName $strRateLimitWaiting",
87 | getString(R.string.FetchingUser, listSize)
88 | )
89 | }
90 |
91 | override fun onUncaughtError() {
92 | this@BlockClearService.onUncaughtError(strServiceName)
93 | }
94 |
95 | override fun onNetworkError(retrySelf: ()->Unit) {
96 | this@BlockClearService.onNetworkError(strServiceName, { retrySelf() })
97 | }
98 | })
99 | }
100 |
101 | return START_REDELIVER_INTENT
102 | }
103 |
104 | }
--------------------------------------------------------------------------------
/WebView/src/main/java/com/sasarinomari/webview/WebViewClient.java:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.webview;
2 |
3 | import android.content.ActivityNotFoundException;
4 | import android.content.Intent;
5 | import android.net.Uri;
6 | import android.provider.Browser;
7 | import android.util.Log;
8 | import android.webkit.WebView;
9 |
10 | /**
11 | * Created by MARi on 2018-01-26.
12 | */
13 |
14 |
15 | class WebViewClient extends android.webkit.WebViewClient
16 | {
17 | private final static String TAG = "BoomWebViewClient";
18 | WebViewClientInterface callback = null;
19 | private String redirectUrl;
20 |
21 | public WebViewClient(WebViewClientInterface callback )
22 | {
23 | this.callback = callback;
24 | }
25 |
26 | @Override
27 | public void onPageFinished( WebView view, String url )
28 | {
29 | super.onPageFinished( view, url );
30 | if ( callback != null )
31 | callback.onPageFinished( url );
32 | }
33 |
34 | @Override
35 | public boolean shouldOverrideUrlLoading( WebView view, String url )
36 | {
37 | if ( url.startsWith( "http://m.facebook.com/share.php" ) )
38 | {
39 | // 페이스북 공유 진입
40 | redirectUrl = view.getUrl( );
41 | return false;
42 | }
43 | else if ( url.startsWith( "https://www.facebook.com/dialog/return/close" ) )
44 | {
45 | view.loadUrl( redirectUrl );
46 | return true;
47 | }
48 | else if ( url.startsWith( "intent:kakaolink://send" ) )
49 | {
50 | try
51 | {
52 | Intent intent = new Intent( Intent.ACTION_VIEW, Uri.parse( url.substring( 7 ) ) ); // mainUrl 앞의 인탠트 부분 제거
53 | intent.addCategory( Intent.CATEGORY_BROWSABLE );
54 | intent.putExtra( Browser.EXTRA_APPLICATION_ID, view.getContext().getPackageName( ) );
55 | view.getContext().startActivity( intent );
56 | } catch ( ActivityNotFoundException e )
57 | {
58 | Intent intent = new Intent( Intent.ACTION_VIEW );
59 | intent.setData( Uri.parse( "market://details?id=" + "com.kakao.talk" ) );
60 | view.getContext().startActivity( intent );
61 | Log.d( TAG, "카카오톡이 설치되어있지 않습니다 : " + e.toString( ) );
62 | } catch ( Exception e )
63 | {
64 | Log.d( TAG, e.toString( ) );
65 | }
66 | return true;
67 | }
68 | else if ( url.startsWith( "tel:" ) )
69 | {
70 | Intent tel = new Intent( Intent.ACTION_DIAL, Uri.parse( url ) );
71 | view.getContext( ).startActivity( tel );
72 | return true;
73 | }
74 | else if ( url.startsWith( "mailto:" ) )
75 | {
76 | String body = "Enter your Question, Enquiry or Feedback below:\n\n";
77 | Intent mail = new Intent( Intent.ACTION_SEND );
78 | mail.setType( "application/octet-stream" );
79 | mail.putExtra( Intent.EXTRA_EMAIL, new String[]{ "email address" } );
80 | mail.putExtra( Intent.EXTRA_SUBJECT, "Subject" );
81 | mail.putExtra( Intent.EXTRA_TEXT, body );
82 | view.getContext( ).startActivity( mail );
83 | return true;
84 | }
85 | else return callback != null && callback.shouldOverrideUrlLoading( url );
86 | }
87 |
88 | @Override
89 | public void onReceivedError( WebView view, int errorCode, String description, String failingUrl )
90 | {
91 | super.onReceivedError( view, errorCode, description, failingUrl );
92 |
93 | switch ( errorCode )
94 | {
95 | case ERROR_AUTHENTICATION: // 서버에서 사용자 인증 실패
96 | case ERROR_BAD_URL: // 잘못된 URL
97 | case ERROR_CONNECT: // 서버로 연결 실패
98 | case ERROR_FAILED_SSL_HANDSHAKE: // SSL handshake 수행 실패
99 | case ERROR_FILE: // 일반 파일 오류
100 | case ERROR_FILE_NOT_FOUND: // 파일을 찾을 수 없습니다
101 | case ERROR_HOST_LOOKUP: // 서버 또는 프록시 호스트 이름 조회 실패
102 | case ERROR_IO: // 서버에서 읽거나 서버로 쓰기 실패
103 | case ERROR_PROXY_AUTHENTICATION: // 프록시에서 사용자 인증 실패
104 | case ERROR_REDIRECT_LOOP: // 너무 많은 리디렉션
105 | case ERROR_TIMEOUT: // 연결 시간 초과
106 | case ERROR_TOO_MANY_REQUESTS: // 페이지 로드중 너무 많은 요청 발생
107 | case ERROR_UNKNOWN: // 일반 오류
108 | case ERROR_UNSUPPORTED_AUTH_SCHEME: // 지원되지 않는 인증 체계
109 | case ERROR_UNSUPPORTED_SCHEME:
110 | //view.loadUrl( "about:blank" );
111 | break;
112 | }
113 | }
114 |
115 | }
116 |
117 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Report/ReportListFragment.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Report
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.TextView
10 | import androidx.fragment.app.Fragment
11 | import com.sasarinomari.tweeper.Authenticate.AuthData
12 | import com.sasarinomari.tweeper.R
13 | import com.sasarinomari.tweeper.Hetzer.HetzerReportActivity
14 | import kotlinx.android.synthetic.main.fragment_report_list.view.*
15 | import java.text.SimpleDateFormat
16 | import java.util.*
17 |
18 | /**
19 | * 안씀!
20 | * 기술적인 문제는 없지만 이거 말고 DefualtRecycleAdapter나 쓰셈 ㅋ
21 | * 사유: 촌티남
22 | */
23 | /**
24 | * 트윗지기 서비스에서 생성된 리포트 조회용 공용 프래그먼트.
25 | * Parameters 전부가 인자로 와야 실행 가능.
26 | *
27 | * ReportActivityName은 아이템 클릭 시 redirect할 class 이름이다.
28 | * Class::class.java.name과 같이 써서 인자로 사용할 수 있다.
29 | *
30 | * 추가적으로 setListViewHeightBasedOnChildren(View) 라는 마법의 함수를 실행하면-
31 | * 이 프래그먼트를 추가한 액티비티에서 스크롤이 내려가버림(?)(???)
32 | * 대체 왜이럼?? 전 몰겟구여 암튼 알아서 스크롤 올리는 코드 넣어 쓰세용ㅋㅋ
33 | */
34 | @Suppress("DEPRECATION")
35 | @Deprecated("DefualtRecycleAdapter")
36 | class ReportListFragment(private val intent: Intent,
37 | private val callback: () -> Unit) : Fragment() {
38 | enum class Parameters {
39 | NoReportDescription,
40 | ReportPrefix, ReportActivityName
41 | }
42 |
43 | private fun checkRequirements(): Boolean {
44 | for(parameter in Parameters.values())
45 | if(!intent.hasExtra(parameter.name)) return false
46 | return true
47 | }
48 |
49 | override fun onCreate(savedInstanceState: Bundle?) {
50 | super.onCreate(savedInstanceState)
51 | if(!checkRequirements()) throw Exception("${this::class.java.name} Requirement not ready.")
52 | }
53 |
54 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
55 | val rootView = inflater.inflate(R.layout.fragment_report_list, container, false);
56 |
57 | rootView.text_noReport.text = intent.getStringExtra(Parameters.NoReportDescription.name)
58 |
59 | val reportPrefix = intent.getStringExtra(Parameters.ReportPrefix.name)!!
60 | val userId = AuthData.Recorder(context!!).getFocusedUser()!!.user!!.id
61 | val reports = ReportInterface(userId, reportPrefix).getReportsWithName(this.context!!)
62 | if (reports.isEmpty()) {
63 | rootView.layout_noReport.visibility = View.VISIBLE
64 | rootView.listView.visibility = View.GONE
65 | } else {
66 | val adapter = object : com.sasarinomari.tweeper.View.DefaultListItem(reports) {
67 | @SuppressLint("SimpleDateFormat")
68 | override fun drawItem(item: Any, title: TextView, description: TextView) {
69 | item as Pair<*, *>
70 | title.text = item.first.toString()
71 | description.text = SimpleDateFormat(getString(R.string.Format_DateTime)).format(item.second as Date)
72 | }
73 | }
74 | rootView.listView.adapter = adapter
75 | rootView.listView.setOnItemClickListener { _, _, index, _ ->
76 | val reportIndex = (adapter.getItem(index) as Pair<*, *>).first.toString().removePrefix(reportPrefix).toInt()
77 | val intent = Intent(this.context,
78 | Class.forName(intent.getStringExtra(Parameters.ReportActivityName.name)!!))
79 | intent.putExtra(HetzerReportActivity.Parameters.ReportId.name, reportIndex)
80 | startActivity(intent)
81 | }
82 | setListViewHeightBasedOnChildren(rootView)
83 | }
84 | return rootView
85 | }
86 |
87 | override fun onResume() {
88 | callback()
89 | super.onResume()
90 | }
91 |
92 | private fun setListViewHeightBasedOnChildren(rootView: View) {
93 | val listAdapter = rootView.listView.adapter ?: return
94 | var totalHeight = 0
95 |
96 | /**
97 | * 모든 listItem의 크기가 같다면 하나만 measure 해도 될 듯함.
98 | * 근데 내가 무슨 부귀영화를 누리겠다고 그렇게까지 최적화를..?
99 | */
100 | for ( i in 0 until listAdapter.count) {
101 | val listItem = listAdapter.getView(i, null, rootView.listView)
102 | listItem.measure(0, 0)
103 | totalHeight += listItem.measuredHeight
104 | }
105 |
106 | val params = rootView.listView.layoutParams
107 | params.height = totalHeight + (rootView.listView.dividerHeight * (listAdapter.count - 1))
108 | rootView.listView.layoutParams = params
109 | rootView.listView.requestLayout()
110 | }
111 |
112 |
113 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
55 |
56 |
59 |
60 |
63 |
64 |
65 |
68 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
82 |
85 |
88 |
91 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_logicpair.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
14 |
15 |
21 |
28 |
36 |
43 |
44 |
45 |
58 |
59 |
63 |
64 |
73 |
74 |
75 |
80 |
85 |
86 |
87 |
88 |
94 |
102 |
103 |
104 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/UITestActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.content.Intent
4 | import android.graphics.Color
5 | import android.os.Bundle
6 | import android.util.Log
7 | import android.view.View
8 | import android.widget.*
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import com.sasarinomari.tweeper.Analytics.AnalyticsReport
11 | import com.sasarinomari.tweeper.Authenticate.AuthData
12 | import com.sasarinomari.tweeper.Base.BaseActivity
13 | import com.sasarinomari.tweeper.Billing.BillingActivity
14 | import com.sasarinomari.tweeper.Hetzer.LogicpairTypeSelectActivity
15 | import com.sasarinomari.tweeper.MediaDownload.MediaDownloadActivity
16 | import com.sasarinomari.tweeper.Report.ReportInterface
17 | import kotlinx.android.synthetic.main.fragment_card_button.view.*
18 | import kotlinx.android.synthetic.main.fragment_column_header.view.*
19 | import kotlinx.android.synthetic.main.fragment_title_with_desc.view.*
20 | import kotlinx.android.synthetic.main.full_recycler_view.*
21 |
22 |
23 | class UITestActivity : BaseActivity() {
24 |
25 | private val LOG_TAG = "UITest"
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 | val root = ListView(this)
30 | setContentView(root)
31 |
32 | val menus = arrayOf(
33 | "test Recycler Inject",
34 | "test Reward Ad",
35 | "enter Billing Activity",
36 | "test Firebase Logging",
37 | "test Media Download",
38 | "check Connection",
39 | "load Analytics Reports",
40 | "LogicPair"
41 | )
42 |
43 | val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, menus)
44 | root.adapter = adapter
45 | root.onItemClickListener = AdapterView.OnItemClickListener { adapterView, view, position, id ->
46 | when(position) {
47 | 0-> testRecyclerInjector()
48 | 1-> testRewardAd()
49 | 2-> testBillingActivity()
50 | 3-> testFirebaseLogging()
51 | 4-> textMediaDownload()
52 | 5-> connectionCheck()
53 | 6-> reportLoadTest()
54 | 7-> textLogicPair()
55 | }
56 | }
57 | }
58 |
59 | private fun textLogicPair() {
60 | startActivity(Intent(this, LogicpairTypeSelectActivity::class.java))
61 | }
62 |
63 | private fun reportLoadTest() {
64 | val reportPrefix = AnalyticsReport.prefix
65 | val userId = AuthData.Recorder(this).getFocusedUser()!!.user!!.id
66 | val reports = ReportInterface(userId, reportPrefix).getReportsWithName(this)
67 |
68 | val list = ListView(this)
69 | setContentView(list)
70 |
71 | val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, reports)
72 | list.adapter = adapter
73 | }
74 |
75 | private fun connectionCheck() {
76 | val result = TwitterAdapter.isConnected(this)
77 | Log.i(LOG_TAG, result.toString())
78 | }
79 |
80 | private fun textMediaDownload() {
81 | startActivity(Intent(this, MediaDownloadActivity::class.java))
82 | }
83 |
84 | private fun testFirebaseLogging() {
85 | fbLog.log("Test_Event",
86 | Pair("Param1", "Hello, World!"),
87 | Pair("Param2", "Second Run"),
88 | Pair("Param3", "Zi be ga go sip da"))
89 | }
90 |
91 | private fun testBillingActivity() {
92 | val intent = Intent(this, BillingActivity::class.java)
93 | startActivity(intent)
94 | }
95 |
96 | private fun testRewardAd() {
97 | RewardedAdAdapter.show(this, object: RewardedAdAdapter.RewardInterface {
98 | override fun onFinished() {
99 | finish()
100 | }
101 | })
102 | }
103 |
104 | private fun testRecyclerInjector() {
105 | setContentView(R.layout.full_recycler_view)
106 |
107 | val adapter = RecyclerInjector()
108 | adapter.add(object: RecyclerInjector.RecyclerFragment(R.layout.fragment_title_with_desc) {
109 | override fun draw(view: View, item: Any?, viewType: Int, listItemIndex: Int) {
110 | view.title_text.text = "테스트 제목"
111 | view.title_description.text = "제목 설명"
112 | }
113 | })
114 | adapter.add(object: RecyclerInjector.RecyclerFragment(R.layout.fragment_card_button) {
115 | override fun draw(view: View, item: Any?, viewType: Int, listItemIndex: Int) {
116 | view.cardbutton_image.setOvalColor(Color.RED)
117 | view.cardbutton_image.setImageResource(R.mipmap.ic_launcher)
118 | view.cardbutton_text.text = "테스트 버튼"
119 | }
120 | })
121 | adapter.add(object: RecyclerInjector.RecyclerFragment(R.layout.fragment_column_header) {
122 | override fun draw(view: View, item: Any?, viewType: Int, listItemIndex: Int) {
123 | view.column_title.text = "헤더 제목"
124 | view.column_description.text = "헤더 설명"
125 | }
126 | })
127 |
128 | root.layoutManager = LinearLayoutManager(this)
129 | root.adapter = adapter
130 | }
131 | }
132 |
133 |
134 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/RecyclerInjector.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import android.widget.FrameLayout
7 | import androidx.recyclerview.widget.RecyclerView
8 |
9 | /**
10 | * 와, 이건 진짜 내가 만들었는데 잘 만든거같음
11 | * 우사긔. 그는 천재인가??
12 | * - 2020.06.28
13 | */
14 | /**
15 | * RecyclerView에 동적으로 ViewHolder를 할당시켜서 사용할 수 있게 해주는 어댑터.
16 | * 일반 뷰와 리스트 모두 삽입 가능.
17 | */
18 | class RecyclerInjector : RecyclerView.Adapter() {
19 | abstract class RecyclerFragment {
20 | constructor(layoutId: Int) { this.layoutId = layoutId}
21 | constructor(layoutId: Int, list: ArrayList<*>): this(layoutId) { bindList(list) }
22 |
23 | private var layoutId: Int = 0
24 | private var _list : ArrayList<*>? = null
25 | var visible: Boolean = true
26 | val count: Int get() {
27 | return if (!visible) 0 else if(isList) _list!!.size else 1
28 | }
29 |
30 | open fun createViewHolder(view: View) : RecyclerView.ViewHolder {
31 | return object: RecyclerView.ViewHolder(view) { }
32 | }
33 | abstract fun draw(view: View, item: Any?, viewType: Int, listItemIndex: Int)
34 | open fun onClickListItem(item: Any?) { }
35 | open fun inflate(parent: ViewGroup): View {
36 | return LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
37 | }
38 |
39 | // region List Methods
40 | fun bindList(list: ArrayList<*>) {
41 | this._list = list
42 | }
43 | val isList: Boolean get() { return _list != null }
44 | fun getListItem(index: Int): Any? {
45 | return if(isList) this._list!![index]
46 | else null
47 | }
48 | fun removeListItem(listItemIndex: Int) {
49 | if(!isList) return
50 | this._list!!.removeAt(listItemIndex)
51 | }
52 | // endregion
53 | }
54 |
55 | private val fragments = ArrayList()
56 |
57 | fun add(rFragment: RecyclerFragment) {
58 | this.fragments.add(rFragment)
59 | }
60 |
61 | fun addSpace(space: Int) {
62 | this.fragments.add(object: RecyclerFragment(0) {
63 | override fun draw(view: View, item: Any?, viewType: Int, listItemIndex: Int) { }
64 |
65 | override fun inflate(parent: ViewGroup): View {
66 | val view = FrameLayout(parent.context)
67 | val params = FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT )
68 | params.setMargins(0,space * 10,0, 0)
69 | view.layoutParams = params
70 | return view
71 | }
72 | })
73 | }
74 |
75 | override fun getItemViewType(position: Int): Int {
76 | var counter = 0
77 | for(i in 0 until fragments.count()) {
78 | val f = fragments[i]
79 | val startIndex = counter
80 | val endIndex = counter + f.count
81 | if(position in startIndex until endIndex) {
82 | return i
83 | }
84 | counter = endIndex
85 | }
86 | throw Exception("Fragment Out Of Range")
87 | }
88 | private fun getItemListIndex(position: Int): Int {
89 | var counter = 0
90 | for(i in 0 until fragments.count()) {
91 | val f = fragments[i]
92 | val startIndex = counter
93 | val endIndex = counter + f.count
94 | if(position in startIndex until endIndex) {
95 | return position - startIndex
96 | }
97 | counter = endIndex
98 | }
99 | throw Exception("Fragment Out Of Range")
100 | }
101 |
102 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
103 | val fragment = fragments[viewType]
104 | val view = fragment.inflate(parent)
105 | return fragment.createViewHolder(view)
106 | }
107 |
108 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
109 | val viewType = getItemViewType(position)
110 | val fragment = fragments[viewType]
111 | if(!fragment.visible) return
112 | val listItemIndex = getItemListIndex(position)
113 | val item = fragment.getListItem(listItemIndex)
114 | fragment.draw(holder.itemView, item, viewType, listItemIndex)
115 | if(fragment.isList) holder.itemView.setOnClickListener { fragment.onClickListItem(item!!) }
116 | }
117 |
118 | fun getItemCount(viewType: Int): Int {
119 | return fragments[viewType].count
120 | }
121 | override fun getItemCount(): Int {
122 | var counter = 0
123 | for(f in fragments) { counter += f.count }
124 | return counter
125 | }
126 | override fun getItemId(position: Int): Long {
127 | return position.toLong()
128 | }
129 |
130 | fun removeListItem(viewType: Int, listItemIndex: Int) {
131 | val f = fragments[viewType]
132 | f.removeListItem(listItemIndex)
133 | }
134 |
135 | fun getFragment(viewType: Int) : RecyclerFragment {
136 | return fragments[viewType]
137 | }
138 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/ScheduledTask/ScheduleManageActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.ScheduledTask
2 |
3 | import android.app.AlarmManager
4 | import android.app.PendingIntent
5 | import android.content.BroadcastReceiver
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.os.Build
9 | import android.os.Bundle
10 | import android.util.Log
11 | import android.view.View
12 | import androidx.appcompat.app.AppCompatActivity
13 | import androidx.recyclerview.widget.LinearLayoutManager
14 | import com.google.gson.Gson
15 | import com.sasarinomari.tweeper.Analytics.AnalyticsService
16 | import com.sasarinomari.tweeper.Authenticate.AuthData
17 | import com.sasarinomari.tweeper.FirebaseLogger
18 | import com.sasarinomari.tweeper.R
19 | import com.sasarinomari.tweeper.RecyclerInjector
20 | import com.sasarinomari.tweeper.TwitterAdapter
21 | import kotlinx.android.synthetic.main.fragment_card_button.view.*
22 | import kotlinx.android.synthetic.main.fragment_title_with_desc.view.*
23 | import kotlinx.android.synthetic.main.full_recycler_view.*
24 | import java.lang.Exception
25 | import java.util.*
26 |
27 | class ScheduleManageActivity : AppCompatActivity() {
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 | setContentView(R.layout.full_recycler_view)
31 |
32 |
33 | val adapter = RecyclerInjector()
34 | adapter.add(object: RecyclerInjector.RecyclerFragment(R.layout.fragment_title_with_desc) {
35 | override fun draw(view: View, item: Any?, viewType: Int, listItemIndex: Int) {
36 | view.title_text.text = "예약 작업 테스트"
37 | view.title_description.text = "지정된 시간에 자동으로 작업을 시작합니다."
38 | }
39 | })
40 | adapter.add(object: RecyclerInjector.RecyclerFragment(R.layout.fragment_card_button) {
41 | override fun draw(view: View, item: Any?, viewType: Int, listItemIndex: Int) {
42 | view.cardbutton_text.text = getString(R.string.TweetCleanerRun)
43 | view.setOnClickListener {
44 |
45 | }
46 | }
47 | })
48 | root.layoutManager = LinearLayoutManager(this)
49 | root.adapter = adapter
50 |
51 | }
52 |
53 | fun alramTest(context: Context) {
54 | val loggedUser = AuthData.Recorder(context).getFocusedUser()
55 |
56 | val intent = Intent(context, MAlarmReceiver::class.java)
57 | intent.putExtra(MAlarmReceiver.Parameters.User.name, Gson().toJson(loggedUser))
58 |
59 | val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
60 | val alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0)
61 |
62 | // TwitterAdapter.twitter.oAuthAccessToken = user.token!!
63 |
64 | // Set the alarm to start at 21:32 PM
65 | val calendar = Calendar.getInstance();
66 | calendar.timeInMillis = System.currentTimeMillis();
67 | calendar.set(Calendar.HOUR_OF_DAY, 21)
68 | calendar.set(Calendar.MINUTE, 32)
69 |
70 | // setRepeating() lets you specify a precise custom interval--in this case,
71 | // 1 day
72 | alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, AlarmManager.INTERVAL_DAY, alarmIntent)
73 | }
74 |
75 | class MAlarmReceiver : BroadcastReceiver() {
76 | enum class Parameters {
77 | User
78 | }
79 |
80 | override fun onReceive(context: Context, intent: Intent) {
81 | if(!AnalyticsService.checkServiceRunning(context)) {
82 | val user = intent.getStringExtra(Parameters.User.name)
83 | val i = Intent(context, AnalyticsService::class.java)
84 | i.putExtra(AnalyticsService.Parameters.User.name, user)
85 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
86 | context.startForegroundService(i)
87 | }
88 | else {
89 | context.startService(i)
90 | }
91 | }
92 | }
93 |
94 | private fun login(context: Context, id: Long, callback: ()-> Unit) {
95 | Thread {
96 | try {
97 | TwitterAdapter().initialize(context, AuthData.Recorder(context).getFocusedUser()!!.token!!).getMe(object : TwitterAdapter.FetchObjectInterface {
98 | override fun onStart() {}
99 |
100 | override fun onFinished(obj: Any) {
101 | callback()
102 | }
103 |
104 | override fun onRateLimit() {
105 | callback()
106 | }
107 |
108 | override fun onUncaughtError() {
109 | TODO("Not yet implemented")
110 | }
111 |
112 | override fun onNetworkError(retry: () -> Unit) {
113 | TODO("Not yet implemented")
114 | }
115 | })
116 | } catch (e: Exception) {
117 | Log.i(this::class.java.name, "유저 id [$id]로의 로그인에 실패했습니다.")
118 | FirebaseLogger(context).log("AuthFailed", Pair("Message", "유저 id [$id]로의 로그인에 실패했습니다."))
119 | }
120 | }.start()
121 | }
122 | }
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/MediaDownload/MediaDownloadActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.MediaDownload
2 |
3 | import android.content.Intent
4 | import android.os.Build
5 | import android.os.Bundle
6 | import android.util.Log
7 | import android.widget.Toast
8 | import androidx.core.content.ContextCompat
9 | import com.google.android.material.textfield.TextInputLayout
10 | import com.sasarinomari.tweeper.Base.BaseActivity
11 | import com.sasarinomari.tweeper.Permission.PermissionHelper
12 | import com.sasarinomari.tweeper.R
13 | import kotlinx.android.synthetic.main.activity_media_download.*
14 | import kotlinx.android.synthetic.main.fragment_card_button.view.*
15 | import kotlinx.android.synthetic.main.fragment_title_with_desc.view.*
16 |
17 | open class MediaDownloadActivity : BaseActivity() {
18 |
19 | private val permissions= arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
20 | android.Manifest.permission.READ_EXTERNAL_STORAGE)
21 |
22 | private var layoutInitialized = false
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | PermissionHelper.activatePermission(this, permissions) {
27 | if(Intent.ACTION_SEND == intent.action) {
28 | /**
29 | * 공유하기를 통해 접근한 경우 인텐트에서 url을 추출해 작업을 시작합니다.
30 | */
31 | if("text/plain" == intent.type) {
32 | val url = intent.getStringExtra(Intent.EXTRA_TEXT)?:return@activatePermission
33 | downloadMedia(url)
34 | finish()
35 | }
36 | }
37 | else {
38 | /**
39 | * 공유하기를 통해 오지 않은 경우 UI를 초기화하고 직접 url을 입력받습니다.
40 | */
41 | setContentView(R.layout.activity_media_download)
42 | setDefaultBoxColor(ContextCompat.getColor(this, R.color.white))
43 | layoutInitialized = true
44 |
45 | layout_title.title_text.text = getString(R.string.MediaDownloader)
46 | layout_title.title_description.text = getString(R.string.MediaDownloaderDescription)
47 |
48 | layout_button.cardbutton_image.setOvalColor(ContextCompat.getColor(this@MediaDownloadActivity, R.color.bluegrey))
49 | layout_button.cardbutton_image.setImageResource(R.drawable.download)
50 | layout_button.cardbutton_text.text = getString(R.string.Download)
51 | layout_button.setOnClickListener {
52 | val url = input_url.text.toString()
53 | downloadMedia(url)
54 | input_url.setText("")
55 | }
56 | }
57 | }
58 | }
59 |
60 | private fun setDefaultBoxColor(color: Int) {
61 | try {
62 | val defaultStrokeColorField = TextInputLayout::class.java.getDeclaredField("defaultStrokeColor")
63 | defaultStrokeColorField.isAccessible = true
64 | defaultStrokeColorField.set(_input1, color)
65 | val defaultTextColorFiled = TextInputLayout::class.java.getDeclaredField("defaultTextColor")
66 | defaultTextColorFiled.isAccessible = true
67 | defaultTextColorFiled.set(input_url, color)
68 | } catch (e: Exception) {
69 | e.printStackTrace()
70 | }
71 | }
72 |
73 |
74 | /**
75 | * 잘못된 url로 요청되었을 경우.
76 | */
77 | private fun invalidUrl() {
78 | runOnUiThread {
79 | if(layoutInitialized) {
80 | da.error(getString(R.string.Error), getString(R.string.InvalidUrl)).show()
81 | }
82 | else {
83 | Toast.makeText(this, getString(R.string.InvalidUrl), Toast.LENGTH_LONG).show()
84 | finish()
85 | }
86 | }
87 | }
88 |
89 | /**
90 | * url로부터 status id만을 추출합니다.
91 | */
92 | private fun getStatusId(url: String): Long {
93 | if(url.isEmpty()) return -1
94 | val b = url.substringAfterLast("/")
95 | val regex = """[0-9]+""".toRegex()
96 | val matchResult = regex.find(b)?.value ?: return -1
97 | return matchResult.toLong()
98 | }
99 |
100 | /**
101 | * url로 파일을 다운로드하는 메인 코드
102 | */
103 | private fun downloadMedia(url: String) {
104 | Log.i("downloadMedia", url)
105 | val id = getStatusId(url)
106 | if (id == (-1).toLong()) {
107 | invalidUrl()
108 | return
109 | }
110 |
111 | val i = Intent(this, MediaDownloadService::class.java)
112 | i.putExtra(MediaDownloadService.Parameters.StatusId.name, id)
113 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
114 | startForegroundService(i)
115 | } else {
116 | startService(i)
117 | }
118 | }
119 |
120 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
121 | // super.onRequestPermissionsResult(requestCode, permissions, grantResults)
122 | PermissionHelper.onRequestPermissionsResult(this, permissions, requestCode, grantResults) {
123 | Toast.makeText(this, getString(R.string.PermissionDenied), Toast.LENGTH_LONG).show()
124 | Log.i("log", getString(R.string.PermissionDenied))
125 | finish()
126 | }
127 | }
128 |
129 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sasarinomari/tweeper/Hetzer/HetzerActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sasarinomari.tweeper.Hetzer
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.os.Build
6 | import android.os.Bundle
7 | import android.view.View
8 | import android.widget.Button
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.google.gson.Gson
12 | import com.sasarinomari.tweeper.Authenticate.AuthData
13 | import com.sasarinomari.tweeper.Base.BaseActivity
14 | import com.sasarinomari.tweeper.R
15 | import com.sasarinomari.tweeper.RecyclerInjector
16 | import com.sasarinomari.tweeper.Report.ReportInterface
17 | import kotlinx.android.synthetic.main.activity_analytics.*
18 | import kotlinx.android.synthetic.main.activity_hetzer.*
19 | import kotlinx.android.synthetic.main.activity_hetzer.layout_button
20 | import kotlinx.android.synthetic.main.activity_hetzer.layout_column_header
21 | import kotlinx.android.synthetic.main.activity_hetzer.layout_recyclerview
22 | import kotlinx.android.synthetic.main.activity_hetzer.layout_title_and_desc
23 | import kotlinx.android.synthetic.main.fragment_column_header.view.*
24 | import kotlinx.android.synthetic.main.fragment_no_item.view.*
25 | import kotlinx.android.synthetic.main.fragment_title_with_desc.view.*
26 | import kotlinx.android.synthetic.main.item_default.view.*
27 | import java.text.SimpleDateFormat
28 | import java.util.*
29 |
30 | class HetzerActivity : BaseActivity() {
31 | enum class RequestCodes {
32 | GetLogics
33 | }
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 | setContentView(R.layout.activity_hetzer)
38 |
39 | val reportPrefix = HetzerReport.prefix
40 | val userId = AuthData.Recorder(this).getFocusedUser()!!.user!!.id
41 | val reports = ReportInterface(userId, reportPrefix).getReportsWithDate(this)
42 | reports.reverse()
43 |
44 | layout_title_and_desc.title_text.text = getString(R.string.TweetCleaner)
45 | layout_title_and_desc.title_description.text = getString(R.string.TweetCleanerDescription)
46 |
47 | layout_column_header.column_title.text = getString(R.string.TweetCleanerReports)
48 | layout_column_header.column_description.text = getString(R.string.TouchToDetail)
49 |
50 | val button = layout_button as Button
51 | button.text = getString(R.string.TweetCleanerRun)
52 | button.setOnClickListener {
53 | if(HetzerService.checkServiceRunning((this@HetzerActivity))) {
54 | da.warning(getString(R.string.Wait), getString(R.string.duplicateService_Hetzer)).show()
55 | }
56 | else {
57 | startActivityForResult(
58 | Intent(this@HetzerActivity, LogicPairActivity::class.java),
59 | RequestCodes.GetLogics.ordinal
60 | )
61 | }
62 | }
63 |
64 | val adapter = RecyclerInjector()
65 | adapter.add(object: RecyclerInjector.RecyclerFragment(R.layout.item_default, reports) {
66 | @SuppressLint("SetTextI18n", "SimpleDateFormat")
67 | override fun draw(view: View, item: Any?, viewType: Int, listItemIndex: Int) {
68 | item as Pair
69 | val block = item.first.split("_")
70 | view.defaultitem_title.text =
71 | if(block.size > 1) getString(R.string.TweetCleanerReport) + ' ' + (block[1].toInt() + 1)
72 | else item.first
73 | view.defaultitem_description.text =
74 | if(item.second != null) SimpleDateFormat("yyyy년 MM월 dd일 hh시 mm분", Locale.KOREA).format(item.second)
75 | else null
76 | }
77 |
78 | override fun onClickListItem(item: Any?) {
79 | val intent = Intent(this@HetzerActivity, HetzerReportActivity::class.java)
80 | intent.putExtra(HetzerReportActivity.Parameters.ReportId.name, (item as Pair).first)
81 | startActivity(intent)
82 | }
83 | })
84 | adapter.add(object: RecyclerInjector.RecyclerFragment(R.layout.fragment_no_item) {
85 | override fun draw(view: View, item: Any?, viewType: Int, listItemIndex: Int) {
86 | val f = adapter.getFragment(viewType - 1)
87 | view.noitem_text.visibility = if(f.visible && f.count == 0) {
88 | view.noitem_text.text = getString(R.string.NoHetzerReports)
89 | View.VISIBLE
90 | } else View.GONE
91 | }
92 | })
93 |
94 | val recycler = layout_recyclerview as RecyclerView
95 | recycler.layoutManager = LinearLayoutManager(this)
96 | recycler.adapter = adapter
97 | }
98 |
99 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
100 | when (requestCode) {
101 | RequestCodes.GetLogics.ordinal -> {
102 | if (resultCode != RESULT_OK) {
103 | return
104 | }
105 | val intent = Intent(this, HetzerService::class.java)
106 | val json = data!!.getStringExtra(HetzerService.Parameters.Logics.name)
107 | intent.putExtra(HetzerService.Parameters.Logics.name, json)
108 | intent.putExtra(HetzerService.Parameters.User.name,
109 | Gson().toJson(AuthData.Recorder(this@HetzerActivity).getFocusedUser()!!))
110 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
111 | startForegroundService(intent)
112 | } else {
113 | startService(intent)
114 | }
115 | setResult(RESULT_OK)
116 | finish()
117 | }
118 | else -> super.onActivityResult(requestCode, resultCode, data)
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------