first commit

This commit is contained in:
Thomas 2022-04-27 15:20:42 +02:00
commit 0f855c5ac2
710 changed files with 112474 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
*.iml
.gradle
/local.properties
/.idea/*
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

122
app/build.gradle Normal file
View file

@ -0,0 +1,122 @@
plugins {
id 'com.android.application'
}
def flavor
android {
compileSdk 31
defaultConfig {
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
flavorDimensions "default"
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
productFlavors {
fdroid {
applicationId "fr.gouv.etalab.mastodon"
buildConfigField "boolean", "DONATIONS", "true"
buildConfigField "boolean", "push", "false"
flavor = "fdroid"
}
playstore {
applicationId "app.fedilab.android"
buildConfigField "boolean", "DONATIONS", "false"
buildConfigField "boolean", "push", "true"
flavor = "playstore"
}
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
buildFeatures {
viewBinding true
}
sourceSets {
playstore {
manifest.srcFile "src/playstore/AndroidManifest.xml"
java.srcDirs = ['src/main/java', 'src/playstore/java']
res.srcDirs = ['src/main/res', 'src/playstore/res']
}
fdroid {
java.srcDirs = ['src/main/java', 'src/fdroid/java']
res.srcDirs = ['src/main/res', 'src/fdroid/res']
}
}
configurations {
all {
exclude group: 'androidx.lifecycle', module: 'lifecycle-viewmodel-ktx'
}
}
}
allprojects {
repositories {
mavenLocal()
maven { url "https://jitpack.io" }
}
}
dependencies {
implementation project(':autoimageslider')
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation "com.google.code.gson:gson:2.8.6"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.preference:preference:1.2.0'
implementation 'com.github.evozi:Cyanea:1.0.7'
implementation 'com.vanniktech:emoji-one:0.6.0'
implementation 'com.github.GrenderG:Toasty:1.5.2'
implementation 'org.framagit.tom79:SparkButton:1.0.13'
implementation "com.github.bumptech.glide:glide:4.12.0"
implementation 'com.github.mergehez:ArgPlayer:v3.1'
implementation ("com.github.bumptech.glide:recyclerview-integration:4.12.0") {
// Excludes the support library because it's already included by Glide.
transitive = false
}
implementation project(path: ':mytransl')
implementation project(path: ':ratethisapp')
annotationProcessor "com.github.bumptech.glide:compiler:4.12.0"
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'com.github.penfeizhou.android.animation:apng:2.17.0'
implementation 'com.github.penfeizhou.android.animation:gif:2.17.0'
implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
implementation 'com.github.piasy:rxandroidaudio:1.7.0'
implementation 'com.github.piasy:AudioProcessor:1.7.0'
implementation "androidx.work:work-runtime:2.7.1"
implementation 'app.futured.hauler:hauler:5.0.0'
implementation "com.github.chrisbanes:PhotoView:2.3.0"
implementation "ch.acra:acra-mail:5.5.0"
implementation "ch.acra:acra-limiter:5.5.0"
implementation "ch.acra:acra-notification:5.5.0"
implementation "com.madgag.spongycastle:bctls-jdk15on:1.58.0.0"
implementation 'com.github.UnifiedPush:android-connector:2.0.0'
// implementation 'com.github.UnifiedPush:android-foss_embedded_fcm_distributor:1.0.0-beta1'
playstoreImplementation 'com.github.UnifiedPush:android-embedded_fcm_distributor:1.1.0'
implementation 'com.burhanrashid52:photoeditor:1.5.1'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
implementation 'androidx.lifecycle:lifecycle-livedata:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.4.1'
implementation 'androidx.navigation:navigation-fragment:2.4.1'
implementation 'androidx.navigation:navigation-ui:2.4.1'
testImplementation 'junit:junit:'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
}

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,26 @@
package app.fedilab.android;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("app.fedilab.android", appContext.getPackageName());
}
}

View file

@ -0,0 +1,27 @@
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
package app.fedilab.android.activities;
import app.fedilab.android.BaseMainActivity;
public class MainActivity extends BaseMainActivity {
@Override
protected void rateThisApp() {
// do nothing
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="my_images"
path="Android/data/fr.gouv.etalab.mastodon/files/Pictures" />
<cache-path
name="*"
path="." />
</paths>

View file

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="app.fedilab.android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name=".MainApplication"
tools:replace="android:allowBackup"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
>
<activity
android:name=".activities.MainActivity"
android:exported="true"
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:exported="true"
android:name=".activities.LoginActivity"
android:configChanges="orientation|screenSize"
android:windowSoftInputMode="stateAlwaysHidden">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="backtofedilab"
android:scheme="fedilab" />
</intent-filter>
</activity>
<activity
android:name=".activities.WebviewConnectActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".activities.ContextActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".activities.DraftActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".activities.ComposeActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="stateVisible"
android:label="@string/compose" />
<activity
android:name=".activities.StatusInfoActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".activities.WebviewActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".activities.ProfileActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/account" />
<activity
android:name=".activities.ScheduledActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/scheduled" />
<service
android:name=".services.PostMessageService"
android:label="@string/post_message" />
<activity
android:name=".activities.SearchResultTabActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/AppThemeBar"
android:label="@string/search" />
<activity
android:name=".activities.ReorderTimelinesActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/reorder_timelines"
android:theme="@style/AppThemeBar" />
<activity
android:name=".activities.ActionActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/interactions"
android:theme="@style/AppThemeBar" />
<activity
android:name=".activities.MastodonListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/action_lists"
android:theme="@style/AppThemeBar" />
<activity
android:name=".activities.SettingsActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/settings"
android:theme="@style/AppThemeBar" />
<activity
android:name=".activities.InstanceActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/action_about_instance"
android:theme="@style/DialogDark" />
<activity
android:name=".activities.InstanceProfileActivity"
android:excludeFromRecents="true"
android:theme="@style/DialogDark" />
<activity
android:name=".activities.ProxyActivity"
android:excludeFromRecents="true"
android:theme="@style/DialogDark" />
<activity
android:name=".activities.HashTagActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".activities.MediaActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/TransparentDark" />
<activity
android:name=".activities.InstanceHealthActivity"
android:excludeFromRecents="true"
android:theme="@style/DialogDark" />
<activity
android:name=".activities.ReportActivity"
android:theme="@style/AppThemeBarDark"
android:windowSoftInputMode="stateVisible" />
<activity
android:name=".activities.CustomSharingActivity"
android:label="@string/settings_title_custom_sharing"
android:windowSoftInputMode="stateVisible"
android:theme="@style/AppThemeBarDark" />
<activity
android:name=".activities.FilterActivity"
android:label="@string/filters"
android:theme="@style/AppThemeBarDark"
android:windowSoftInputMode="stateVisible" />
<activity
android:name=".activities.EditProfileActivity"
android:label="@string/edit_profile"
android:theme="@style/AppThemeBarDark"
android:windowSoftInputMode="stateVisible" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<receiver
android:name=".broadcastreceiver.ToastMessage"
android:exported="false">
<intent-filter>
<action android:name="RECEIVE_TOAST_MESSAGE" />
</intent-filter>
</receiver>
<receiver
android:name=".services.CustomReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
</intent-filter>
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,13 @@
base_theme,2
author,Fedilab
name,Breeze Dark - Yellow
theme_boost_header_color,-14012878
theme_statuses_color,-14473687
theme_link_color,-12734743
theme_icons_color,-4340793
pref_color_background,-15658735
pref_color_navigation_bar,true
pref_color_status_bar,true
theme_accent,-148405
theme_text_color,-1052431
theme_primary,-13552069
1 base_theme 2
2 author Fedilab
3 name Breeze Dark - Yellow
4 theme_boost_header_color -14012878
5 theme_statuses_color -14473687
6 theme_link_color -12734743
7 theme_icons_color -4340793
8 pref_color_background -15658735
9 pref_color_navigation_bar true
10 pref_color_status_bar true
11 theme_accent -148405
12 theme_text_color -1052431
13 theme_primary -13552069

View file

@ -0,0 +1,15 @@
base_theme,2
author,Roboron
name,Cyberpunk Neon
theme_boost_header_color,-16776697,
theme_text_header_1_line,-1441575,
theme_text_header_2_line,-5242717,
theme_statuses_color,-16181197,
theme_link_color,-1441575,
theme_icons_color,-16138810,
pref_color_background,-16774370,
pref_color_navigation_bar,true,
pref_color_status_bar,true,
theme_accent,-1441575,
theme_text_color,-16138810,
theme_primary,-16774370,
1 base_theme,2
2 author,Roboron
3 name,Cyberpunk Neon
4 theme_boost_header_color,-16776697,
5 theme_text_header_1_line,-1441575,
6 theme_text_header_2_line,-5242717,
7 theme_statuses_color,-16181197,
8 theme_link_color,-1441575,
9 theme_icons_color,-16138810,
10 pref_color_background,-16774370,
11 pref_color_navigation_bar,true,
12 pref_color_status_bar,true,
13 theme_accent,-1441575,
14 theme_text_color,-16138810,
15 theme_primary,-16774370,

View file

@ -0,0 +1,15 @@
base_theme,2
author,Jøta Seth
name,Grey Orange
theme_boost_header_color,-14869219
theme_text_header_1_line,-1
theme_text_header_2_line,-1
theme_statuses_color,-14145496
theme_link_color,-26624
theme_icons_color,-26624
pref_color_background,-13092808
pref_color_navigation_bar,true
pref_color_status_bar,true
theme_accent,-26624
theme_text_color,-1
theme_primary,-14408668
1 base_theme 2
2 author Jøta Seth
3 name Grey Orange
4 theme_boost_header_color -14869219
5 theme_text_header_1_line -1
6 theme_text_header_2_line -1
7 theme_statuses_color -14145496
8 theme_link_color -26624
9 theme_icons_color -26624
10 pref_color_background -13092808
11 pref_color_navigation_bar true
12 pref_color_status_bar true
13 theme_accent -26624
14 theme_text_color -1
15 theme_primary -14408668

View file

@ -0,0 +1,15 @@
base_theme,2
author,@AntoineD@h.kher.nl
name,Gruvbox OLED
theme_boost_header_color,-16777216
theme_text_header_1_line,-265785
theme_text_header_2_line,-6777062
theme_statuses_color,-16777216
theme_link_color,-2647775
theme_icons_color,-7175308
pref_color_background,-16777216
pref_color_navigation_bar,true
pref_color_status_bar,true
theme_accent,-9921174
theme_text_color,-265785
theme_primary,-16777216
1 base_theme 2
2 author @AntoineD@h.kher.nl
3 name Gruvbox OLED
4 theme_boost_header_color -16777216
5 theme_text_header_1_line -265785
6 theme_text_header_2_line -6777062
7 theme_statuses_color -16777216
8 theme_link_color -2647775
9 theme_icons_color -7175308
10 pref_color_background -16777216
11 pref_color_navigation_bar true
12 pref_color_status_bar true
13 theme_accent -9921174
14 theme_text_color -265785
15 theme_primary -16777216

View file

@ -0,0 +1,15 @@
base_theme,2
author,AngryTux
name,Less Angry Orange
theme_boost_header_color,-15855063
theme_text_header_1_line,-2128640
theme_text_header_2_line,-5329234
theme_statuses_color,-1
theme_link_color,-12146699
theme_icons_color,-2128640
pref_color_background,-15987700
pref_color_navigation_bar,true
pref_color_status_bar,true
theme_accent,-3968000
theme_text_color,-197380
theme_primary,-14408668
1 base_theme 2
2 author AngryTux
3 name Less Angry Orange
4 theme_boost_header_color -15855063
5 theme_text_header_1_line -2128640
6 theme_text_header_2_line -5329234
7 theme_statuses_color -1
8 theme_link_color -12146699
9 theme_icons_color -2128640
10 pref_color_background -15987700
11 pref_color_navigation_bar true
12 pref_color_status_bar true
13 theme_accent -3968000
14 theme_text_color -197380
15 theme_primary -14408668

View file

@ -0,0 +1,15 @@
base_theme,2
author,Mondstern
name,Mondstern Fedilab
theme_boost_header_color,-1,
theme_text_header_1_line,-13855804,
theme_text_header_2_line,-16227945,
theme_statuses_color,-14935012,
theme_link_color,-15542685,
theme_icons_color,-10723999,
pref_color_background,-15921907,
pref_color_navigation_bar,false,
pref_color_status_bar,false,
theme_accent,-15542685,
theme_text_color,-1,
theme_primary,-14474461,
1 base_theme,2
2 author,Mondstern
3 name,Mondstern Fedilab
4 theme_boost_header_color,-1,
5 theme_text_header_1_line,-13855804,
6 theme_text_header_2_line,-16227945,
7 theme_statuses_color,-14935012,
8 theme_link_color,-15542685,
9 theme_icons_color,-10723999,
10 pref_color_background,-15921907,
11 pref_color_navigation_bar,false,
12 pref_color_status_bar,false,
13 theme_accent,-15542685,
14 theme_text_color,-1,
15 theme_primary,-14474461,

View file

@ -0,0 +1,13 @@
base_theme,2
author,Fedilab
name,Nocturnal
theme_boost_header_color,-12895429
theme_statuses_color,-13553359
theme_link_color,-16747570
theme_icons_color,-10158118
pref_color_background,-14606047
pref_color_navigation_bar,true
pref_color_status_bar,true
theme_accent,-13136013
theme_text_color,-2236963
theme_primary,-14013910
1 base_theme 2
2 author Fedilab
3 name Nocturnal
4 theme_boost_header_color -12895429
5 theme_statuses_color -13553359
6 theme_link_color -16747570
7 theme_icons_color -10158118
8 pref_color_background -14606047
9 pref_color_navigation_bar true
10 pref_color_status_bar true
11 theme_accent -13136013
12 theme_text_color -2236963
13 theme_primary -14013910

View file

@ -0,0 +1,15 @@
base_theme,2
author,Jøta Seth
name,Photon Dark
theme_boost_header_color,-14145496
theme_text_header_1_line,-1
theme_text_header_2_line,-1
theme_statuses_color,-14935012
theme_link_color,-14059009
theme_icons_color,-9474193
pref_color_background,-15921907
pref_color_navigation_bar,true
pref_color_status_bar,true
theme_accent,-14059009
theme_text_color,-1
theme_primary,-14474461
1 base_theme 2
2 author Jøta Seth
3 name Photon Dark
4 theme_boost_header_color -14145496
5 theme_text_header_1_line -1
6 theme_text_header_2_line -1
7 theme_statuses_color -14935012
8 theme_link_color -14059009
9 theme_icons_color -9474193
10 pref_color_background -15921907
11 pref_color_navigation_bar true
12 pref_color_status_bar true
13 theme_accent -14059009
14 theme_text_color -1
15 theme_primary -14474461

View file

@ -0,0 +1,15 @@
base_theme,2
author,Fedilab
name,Solarized Dark - Purple
theme_boost_header_color,-16506327
theme_text_header_1_line,-1120043
theme_text_header_2_line,-1120043
theme_statuses_color,-16304574
theme_link_color,-14251054
theme_icons_color,-7102047
pref_color_background,-16766154
pref_color_navigation_bar,true
pref_color_status_bar,true
theme_accent,-9670204
theme_text_color,-133405
theme_primary,-16304574
1 base_theme 2
2 author Fedilab
3 name Solarized Dark - Purple
4 theme_boost_header_color -16506327
5 theme_text_header_1_line -1120043
6 theme_text_header_2_line -1120043
7 theme_statuses_color -16304574
8 theme_link_color -14251054
9 theme_icons_color -7102047
10 pref_color_background -16766154
11 pref_color_navigation_bar true
12 pref_color_status_bar true
13 theme_accent -9670204
14 theme_text_color -133405
15 theme_primary -16304574

View file

@ -0,0 +1,47 @@
[
{
"theme_name": "Dark",
"base_theme": "DARK",
"primary": "#FF272727",
"primary_dark": "#FF272727",
"primary_light": "#FFd9e1e8",
"accent": "#FF2b90d9",
"accent_dark": "#FF1b80c9",
"accent_light": "#FF772b90d9",
"background": "#FF121212",
"background_dark": "#FF282c37",
"background_light": "#FF282c37",
"should_tint_statusbar": true,
"should_tint_navbar": true
},
{
"theme_name": "Light",
"base_theme": "LIGHT",
"primary": "#FFFFFF",
"primary_dark": "#FFFFFFFF",
"primary_light": "#FFd9e1e8",
"accent": "#FF2b90d9",
"accent_dark": "#FF1b80c9",
"accent_light": "#FF772b90d9",
"background": "#FFFFFFFF",
"background_dark": "#FFFFFFFF",
"background_light": "#FFFFFFFF",
"should_tint_statusbar": true,
"should_tint_navbar": true
},
{
"theme_name": "Black",
"base_theme": "DARK",
"primary": "#FF000000",
"primary_dark": "#FF000000",
"primary_light": "#FF000000",
"accent": "#FF606984",
"accent_dark": "#FF606984",
"accent_light": "#FF606984",
"background": "#FF000000",
"background_dark": "#FF000000",
"background_light": "#FF000000",
"should_tint_statusbar": true,
"should_tint_navbar": true
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,677 @@
package app.fedilab.android;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.BaseMainActivity.status.DISCONNECTED;
import static app.fedilab.android.BaseMainActivity.status.UNKNOWN;
import static app.fedilab.android.helper.Helper.PREF_USER_TOKEN;
import static app.fedilab.android.helper.Helper.deleteDir;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.ContextThemeWrapper;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.SearchView;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.view.GravityCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import androidx.preference.PreferenceManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.jaredrummler.cyanea.Cyanea;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.List;
import app.fedilab.android.activities.ActionActivity;
import app.fedilab.android.activities.BaseActivity;
import app.fedilab.android.activities.ComposeActivity;
import app.fedilab.android.activities.DraftActivity;
import app.fedilab.android.activities.FilterActivity;
import app.fedilab.android.activities.InstanceActivity;
import app.fedilab.android.activities.InstanceHealthActivity;
import app.fedilab.android.activities.LoginActivity;
import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.activities.MastodonListActivity;
import app.fedilab.android.activities.ProfileActivity;
import app.fedilab.android.activities.ProxyActivity;
import app.fedilab.android.activities.ReorderTimelinesActivity;
import app.fedilab.android.activities.ScheduledActivity;
import app.fedilab.android.activities.SearchResultTabActivity;
import app.fedilab.android.activities.SettingsActivity;
import app.fedilab.android.broadcastreceiver.NetworkStateReceiver;
import app.fedilab.android.client.entities.Account;
import app.fedilab.android.client.entities.Pinned;
import app.fedilab.android.client.entities.app.PinnedTimeline;
import app.fedilab.android.client.mastodon.entities.Filter;
import app.fedilab.android.client.mastodon.entities.Instance;
import app.fedilab.android.client.mastodon.entities.MastodonList;
import app.fedilab.android.databinding.ActivityMainBinding;
import app.fedilab.android.databinding.NavHeaderMainBinding;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.PinnedTimelineHelper;
import app.fedilab.android.helper.PushHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.viewmodel.mastodon.InstancesVM;
import app.fedilab.android.viewmodel.mastodon.TimelinesVM;
import app.fedilab.android.viewmodel.mastodon.TopBarVM;
import es.dmoral.toasty.Toasty;
public abstract class BaseMainActivity extends BaseActivity implements NetworkStateReceiver.NetworkStateReceiverListener {
public static String currentInstance, currentToken, currentUserID, client_id, client_secret, software;
public static Account.API api;
public static boolean admin;
public static WeakReference<Account> accountWeakReference;
public static HashMap<Integer, Fragment> mPageReferenceMap;
public static status networkAvailable = UNKNOWN;
public static Instance instanceInfo;
public static List<Filter> mainFilters;
public static boolean filterFetched;
Fragment currentFragment;
private Account account;
private AppBarConfiguration mAppBarConfiguration;
private ActivityMainBinding binding;
private Pinned pinned;
private final BroadcastReceiver broadcast_data = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Bundle b = intent.getExtras();
if (b != null) {
if (b.getBoolean(Helper.RECEIVE_REDRAW_TOPBAR, false)) {
List<MastodonList> mastodonLists = (List<MastodonList>) b.getSerializable(Helper.RECEIVE_MASTODON_LIST);
redrawPinned(mastodonLists);
} else if (b.getBoolean(Helper.RECEIVE_RECREATE_ACTIVITY, false)) {
Cyanea.getInstance().edit().apply().recreate(BaseMainActivity.this);
}
}
}
};
private NetworkStateReceiver networkStateReceiver;
private boolean headerMenuOpen;
protected abstract void rateThisApp();
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
mamageNewIntent(intent);
}
@SuppressLint("ApplySharedPref")
private void mamageNewIntent(Intent intent) {
if (intent == null)
return;
String action = intent.getAction();
String type = intent.getType();
Bundle extras = intent.getExtras();
String userIdIntent, instanceIntent;
if (extras != null && extras.containsKey(Helper.INTENT_ACTION)) {
userIdIntent = extras.getString(Helper.PREF_KEY_ID); //Id of the account in the intent
instanceIntent = extras.getString(Helper.PREF_INSTANCE);
if (extras.getInt(Helper.INTENT_ACTION) == Helper.NOTIFICATION_INTENT) {
try {
Account account = new Account(BaseMainActivity.this).getUniqAccount(userIdIntent, instanceIntent);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(BaseMainActivity.this);
headerMenuOpen = false;
Toasty.info(BaseMainActivity.this, getString(R.string.toast_account_changed, "@" + account.mastodon_account.acct + "@" + account.instance), Toasty.LENGTH_LONG).show();
BaseMainActivity.currentToken = account.token;
BaseMainActivity.currentUserID = account.user_id;
api = account.api;
SharedPreferences.Editor editor = sharedpreferences.edit();
editor.putString(PREF_USER_TOKEN, account.token);
editor.commit();
Intent mainActivity = new Intent(this, MainActivity.class);
startActivity(mainActivity);
intent.removeExtra(Helper.INTENT_ACTION);
finish();
} catch (DBException e) {
e.printStackTrace();
}
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(BaseMainActivity.this);
ThemeHelper.applyTheme(this);
if (!Helper.isLoggedIn(BaseMainActivity.this)) {
//It is not, the user is redirected to the login page
Intent myIntent = new Intent(BaseMainActivity.this, LoginActivity.class);
startActivity(myIntent);
finish();
return;
} else {
BaseMainActivity.currentToken = sharedpreferences.getString(Helper.PREF_USER_TOKEN, null);
}
mamageNewIntent(getIntent());
ThemeHelper.initiliazeColors(BaseMainActivity.this);
filterFetched = false;
networkStateReceiver = new NetworkStateReceiver();
networkStateReceiver.addListener(this);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
ActionBar actionBar = getSupportActionBar();
//Remove title
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
}
rateThisApp();
SharedPreferences cyneaPref = getSharedPreferences("com.jaredrummler.cyanea", Context.MODE_PRIVATE);
binding.tabLayout.setTabTextColors(ThemeHelper.getAttColor(BaseMainActivity.this, R.attr.mTextColor), cyneaPref.getInt("theme_accent", -1));
binding.tabLayout.setTabIconTint(ThemeHelper.getColorStateList(BaseMainActivity.this));
binding.compose.setOnClickListener(v -> startActivity(new Intent(this, ComposeActivity.class)));
headerMenuOpen = false;
binding.bottomNavView.inflateMenu(R.menu.bottom_nav_menu);
binding.bottomNavView.setOnItemSelectedListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.nav_home) {
binding.viewPager.setCurrentItem(0);
} else if (itemId == R.id.nav_local) {
binding.viewPager.setCurrentItem(1);
} else if (itemId == R.id.nav_public) {
binding.viewPager.setCurrentItem(2);
} else if (itemId == R.id.nav_notifications) {
binding.viewPager.setCurrentItem(3);
} else if (itemId == R.id.nav_privates) {
binding.viewPager.setCurrentItem(4);
}
return true;
});
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder()
.setOpenableLayout(binding.drawerLayout)
.build();
NavHeaderMainBinding headerMainBinding = NavHeaderMainBinding.inflate(getLayoutInflater());
binding.navView.addHeaderView(headerMainBinding.getRoot());
binding.navView.setNavigationItemSelectedListener(menuItem -> {
int id = menuItem.getItemId();
if (id == R.id.nav_drafts) {
Intent intent = new Intent(this, DraftActivity.class);
startActivity(intent);
} else if (id == R.id.nav_reorder) {
Intent intent = new Intent(this, ReorderTimelinesActivity.class);
startActivity(intent);
} else if (id == R.id.nav_interactions) {
Intent intent = new Intent(this, ActionActivity.class);
startActivity(intent);
} else if (id == R.id.nav_filter) {
Intent intent = new Intent(this, FilterActivity.class);
startActivity(intent);
} else if (id == R.id.nav_list) {
Intent intent = new Intent(this, MastodonListActivity.class);
startActivity(intent);
} else if (id == R.id.nav_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
} else if (id == R.id.nav_scheduled) {
Intent intent = new Intent(this, ScheduledActivity.class);
startActivity(intent);
}
binding.drawerLayout.close();
return false;
});
headerMainBinding.instanceInfoContainer.setOnClickListener(v -> startActivity(new Intent(BaseMainActivity.this, InstanceHealthActivity.class)));
headerMainBinding.accountProfilePicture.setOnClickListener(v -> {
Intent intent = new Intent(BaseMainActivity.this, ProfileActivity.class);
Bundle b = new Bundle();
b.putSerializable(Helper.ARG_ACCOUNT, account.mastodon_account);
intent.putExtras(b);
ActivityOptionsCompat options = ActivityOptionsCompat
.makeSceneTransitionAnimation(BaseMainActivity.this, headerMainBinding.instanceInfoContainer, getString(R.string.activity_porfile_pp));
startActivity(intent, options.toBundle());
});
headerMainBinding.changeAccount.setOnClickListener(v -> {
headerMenuOpen = !headerMenuOpen;
if (headerMenuOpen) {
headerMainBinding.ownerAccounts.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24);
new Thread(() -> {
try {
List<Account> accounts = new Account(BaseMainActivity.this).getAll();
Handler mainHandler = new Handler(Looper.getMainLooper());
Runnable myRunnable = () -> {
binding.navView.getMenu().clear();
binding.navView.inflateMenu(R.menu.menu_accounts);
headerMenuOpen = true;
Menu mainMenu = binding.navView.getMenu();
SubMenu currentSubmenu = null;
String lastInstance = "";
if (accounts != null) {
for (final Account account : accounts) {
if (!currentToken.equalsIgnoreCase(account.token)) {
if (!lastInstance.trim().equalsIgnoreCase(account.instance.trim())) {
lastInstance = account.instance.toUpperCase();
currentSubmenu = mainMenu.addSubMenu(account.instance.toUpperCase());
}
if (currentSubmenu == null) {
continue;
}
final MenuItem item = currentSubmenu.add("@" + account.mastodon_account.acct);
item.setIcon(R.drawable.ic_person);
boolean disableGif = sharedpreferences.getBoolean(getString(R.string.SET_DISABLE_GIF), false);
String url = !disableGif ? account.mastodon_account.avatar : account.mastodon_account.avatar_static;
if (url.startsWith("/")) {
url = "https://" + account.instance + account.mastodon_account.avatar;
}
if (!this.isDestroyed() && !this.isFinishing()) {
if (url.contains(".gif")) {
Glide.with(BaseMainActivity.this)
.asGif()
.load(url)
.into(new CustomTarget<GifDrawable>() {
@Override
public void onResourceReady(@NonNull GifDrawable resource, Transition<? super GifDrawable> transition) {
item.setIcon(resource);
item.getIcon().setColorFilter(0xFFFFFFFF, PorterDuff.Mode.MULTIPLY);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
} else {
Glide.with(BaseMainActivity.this)
.asDrawable()
.load(url)
.into(new CustomTarget<Drawable>() {
@Override
public void onResourceReady(@NonNull Drawable resource, Transition<? super Drawable> transition) {
item.setIcon(resource);
item.getIcon().setColorFilter(0xFFFFFFFF, PorterDuff.Mode.MULTIPLY);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
}
}
item.setOnMenuItemClickListener(item1 -> {
if (!this.isFinishing()) {
headerMenuOpen = false;
Toasty.info(BaseMainActivity.this, getString(R.string.toast_account_changed, "@" + account.mastodon_account.acct + "@" + account.instance), Toasty.LENGTH_LONG).show();
BaseMainActivity.currentToken = account.token;
BaseMainActivity.currentUserID = account.user_id;
api = account.api;
SharedPreferences.Editor editor = sharedpreferences.edit();
editor.putString(PREF_USER_TOKEN, account.token);
editor.commit();
//The user is now aut
//The user is now authenticated, it will be redirected to MainActivity
Intent mainActivity = new Intent(this, MainActivity.class);
startActivity(mainActivity);
finish();
headerMainBinding.ownerAccounts.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24);
return true;
}
return false;
});
}
}
}
currentSubmenu = mainMenu.addSubMenu("");
MenuItem addItem = currentSubmenu.add(R.string.add_account);
addItem.setIcon(R.drawable.ic_baseline_person_add_24);
addItem.setOnMenuItemClickListener(item -> {
Intent intent = new Intent(BaseMainActivity.this, LoginActivity.class);
startActivity(intent);
return true;
});
};
mainHandler.post(myRunnable);
} catch (DBException e) {
e.printStackTrace();
}
}).start();
} else {
binding.navView.getMenu().clear();
binding.navView.inflateMenu(R.menu.activity_main_drawer);
headerMainBinding.ownerAccounts.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24);
headerMenuOpen = false;
}
});
headerMainBinding.headerOptionInfo.setOnClickListener(v -> {
PopupMenu popup = new PopupMenu(new ContextThemeWrapper(BaseMainActivity.this, Helper.popupStyle()), headerMainBinding.headerOptionInfo);
popup.getMenuInflater()
.inflate(R.menu.main, popup.getMenu());
popup.setOnMenuItemClickListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.action_logout_account) {
AlertDialog.Builder alt_bld = new AlertDialog.Builder(BaseMainActivity.this, Helper.dialogStyle());
alt_bld.setTitle(R.string.action_logout);
alt_bld.setMessage(getString(R.string.logout_account_confirmation, account.mastodon_account.username, account.instance));
alt_bld.setPositiveButton(R.string.action_logout, (dialog, id) -> {
dialog.dismiss();
try {
Helper.removeAccount(BaseMainActivity.this, null);
} catch (DBException e) {
e.printStackTrace();
}
});
alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss());
AlertDialog alert = alt_bld.create();
alert.show();
return true;
} else if (itemId == R.id.action_about_instance) {
Intent intent = new Intent(BaseMainActivity.this, InstanceActivity.class);
startActivity(intent);
return true;
} else if (itemId == R.id.action_cache) {
new Helper.CacheTask(BaseMainActivity.this);
return true;
} else if (itemId == R.id.action_proxy) {
Intent intent = new Intent(BaseMainActivity.this, ProxyActivity.class);
startActivity(intent);
return true;
}
return true;
});
popup.show();
});
account = null;
//Update account details
new Thread(() -> {
try {
account = new Account(BaseMainActivity.this).getConnectedAccount();
} catch (DBException e) {
e.printStackTrace();
}
Handler mainHandler = new Handler(Looper.getMainLooper());
Runnable myRunnable = () -> {
if (account == null) {
//It is not, the user is redirected to the login page
Intent myIntent = new Intent(BaseMainActivity.this, LoginActivity.class);
startActivity(myIntent);
finish();
return;
}
currentInstance = account.instance;
currentUserID = account.user_id;
accountWeakReference = new WeakReference<>(account);
binding.profilePicture.setOnClickListener(v -> binding.drawerLayout.openDrawer(GravityCompat.START));
Helper.loadPP(binding.profilePicture, account);
headerMainBinding.accountAcc.setText(String.format("%s@%s", account.mastodon_account.username, account.instance));
if (account.mastodon_account.display_name.isEmpty()) {
account.mastodon_account.display_name = account.mastodon_account.acct;
}
headerMainBinding.accountName.setText(account.mastodon_account.display_name);
Helper.loadPP(headerMainBinding.accountProfilePicture, account);
/*
* Some general data are loaded when the app starts such;
* - Instance info (for limits)
* - Emoji for picker
* - Filters for timelines
* - Pinned timelines (in app feature)
*/
//Update emoji in db for the current instance
new ViewModelProvider(BaseMainActivity.this).get(InstancesVM.class).getEmoji(currentInstance);
//Retrieve instance info
new ViewModelProvider(BaseMainActivity.this).get(InstancesVM.class).getInstance(currentInstance)
.observe(BaseMainActivity.this, instance -> instanceInfo = instance.info);
//Retrieve filters
new ViewModelProvider(BaseMainActivity.this).get(AccountsVM.class).getFilters(currentInstance, currentToken)
.observe(BaseMainActivity.this, filters -> mainFilters = filters);
new ViewModelProvider(BaseMainActivity.this).get(AccountsVM.class).getConnectedAccount(currentInstance, currentToken)
.observe(BaseMainActivity.this, account1 -> {
BaseMainActivity.accountWeakReference.get().mastodon_account = account1;
});
//Update pinned timelines
new ViewModelProvider(BaseMainActivity.this).get(TopBarVM.class).getDBPinned()
.observe(this, pinned -> {
this.pinned = pinned;
//First it's taken from db (last stored values)
PinnedTimelineHelper.redrawTopBarPinned(BaseMainActivity.this, binding, pinned, null);
//Fetch remote lists for the authenticated account and update them
new ViewModelProvider(BaseMainActivity.this).get(TimelinesVM.class).getLists(currentInstance, currentToken)
.observe(this, mastodonLists -> PinnedTimelineHelper.redrawTopBarPinned(BaseMainActivity.this, binding, pinned, mastodonLists));
});
};
mainHandler.post(myRunnable);
}).start();
//Toolbar search
binding.toolbarSearch.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
//Hide keyboard
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(binding.toolbarSearch.getWindowToken(), 0);
query = query.replaceAll("^#+", "");
Intent intent;
intent = new Intent(BaseMainActivity.this, SearchResultTabActivity.class);
intent.putExtra(Helper.ARG_SEARCH_KEYWORD, query);
startActivity(intent);
binding.toolbarSearch.setQuery("", false);
binding.toolbarSearch.setIconified(true);
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
binding.toolbarSearch.setOnCloseListener(() -> {
binding.tabLayout.setVisibility(View.VISIBLE);
return false;
});
PushHelper.startStreaming(BaseMainActivity.this);
binding.toolbarSearch.setOnSearchClickListener(v -> binding.tabLayout.setVisibility(View.VISIBLE));
//For receiving data from other activities
LocalBroadcastManager.getInstance(BaseMainActivity.this).registerReceiver(broadcast_data, new IntentFilter(Helper.BROADCAST_DATA));
}
public void refreshFragment() {
if (binding.viewPager.getAdapter() != null) {
binding.viewPager.getAdapter().notifyDataSetChanged();
}
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(BaseMainActivity.this).unregisterReceiver(broadcast_data);
if (networkStateReceiver != null) {
try {
unregisterReceiver(networkStateReceiver);
} catch (IllegalArgumentException illegalArgumentException) {
illegalArgumentException.printStackTrace();
}
}
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(BaseMainActivity.this);
boolean clearCacheExit = sharedpreferences.getBoolean(getString(R.string.SET_CLEAR_CACHE_EXIT), false);
//Clear cache when leaving - Default = false
if (clearCacheExit) {
new Thread(() -> {
try {
if (getCacheDir().getParentFile() != null) {
String path = getCacheDir().getParentFile().getPath();
File dir = new File(path);
if (dir.isDirectory()) {
deleteDir(dir);
}
}
} catch (Exception ignored) {
}
}).start();
}
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
}
public void redrawPinned(List<MastodonList> mastodonLists) {
int currentItem = binding.viewPager.getCurrentItem();
new ViewModelProvider(BaseMainActivity.this).get(TopBarVM.class).getDBPinned()
.observe(this, pinned -> {
this.pinned = pinned;
//First it's taken from db (last stored values)
PinnedTimelineHelper.redrawTopBarPinned(BaseMainActivity.this, binding, pinned, mastodonLists);
binding.viewPager.setCurrentItem(currentItem);
});
}
@Override
public void onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawer(GravityCompat.START);
} else {
int fragments = getSupportFragmentManager().getBackStackEntryCount();
if (fragments == 1) {
finish();
} else if (getSupportFragmentManager().getBackStackEntryCount() > 1) {
getSupportFragmentManager().popBackStack();
List<Fragment> fragmentList = getSupportFragmentManager().getFragments();
for (Fragment fragment : fragmentList) {
if (fragment != null && fragment.isVisible()) {
if (fragment instanceof FragmentMastodonTimeline) {
currentFragment = fragment;
getSupportFragmentManager().beginTransaction().show(currentFragment).commit();
}
}
}
} else {
super.onBackPressed();
}
}
}
public boolean getFloatingVisibility() {
return binding.compose.getVisibility() == View.VISIBLE;
}
public void manageFloatingButton(boolean display) {
if (display) {
binding.compose.show();
} else {
binding.compose.hide();
}
}
/* @Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_logout) {
AlertDialog.Builder alt_bld = new AlertDialog.Builder(BaseMainActivity.this, Helper.dialogStyle());
alt_bld.setTitle(R.string.action_logout);
alt_bld.setMessage(getString(R.string.logout_account_confirmation, account.mastodon_account.username, account.instance));
alt_bld.setPositiveButton(R.string.action_logout, (dialog, id) -> {
dialog.dismiss();
try {
Helper.removeAccount(BaseMainActivity.this, null);
} catch (DBException e) {
e.printStackTrace();
}
});
alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss());
AlertDialog alert = alt_bld.create();
alert.show();
}
return true;
}*/
@Override
public boolean onSupportNavigateUp() {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
//unselect all tag elements
for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) {
pinnedTimeline.isSelected = false;
}
return NavigationUI.navigateUp(navController, mAppBarConfiguration)
|| super.onSupportNavigateUp();
}
@Override
public void networkAvailable() {
networkAvailable = status.CONNECTED;
}
@Override
public void networkUnavailable() {
networkAvailable = DISCONNECTED;
}
public enum status {
UNKNOWN,
CONNECTED,
DISCONNECTED
}
}

View file

@ -0,0 +1,29 @@
package app.fedilab.android;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import app.fedilab.android.client.entities.InstanceSocial;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Query;
public interface InstancesSocialService {
@GET("instances/search?name=true")
Call<InstanceSocial> getInstances(@Header("Authorization") String token, @Query("q") String search);
}

View file

@ -0,0 +1,90 @@
package app.fedilab.android;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.Context;
import android.content.SharedPreferences;
import android.os.StrictMode;
import androidx.multidex.MultiDex;
import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;
import com.jaredrummler.cyanea.Cyanea;
import com.jaredrummler.cyanea.prefs.CyaneaTheme;
import org.acra.ACRA;
import org.acra.annotation.AcraNotification;
import org.acra.config.CoreConfigurationBuilder;
import org.acra.config.LimiterConfigurationBuilder;
import org.acra.config.MailSenderConfigurationBuilder;
import org.acra.data.StringFormat;
import java.util.List;
import es.dmoral.toasty.Toasty;
@AcraNotification(
resIcon = R.mipmap.ic_launcher, resTitle = R.string.crash_title, resChannelName = R.string.set_crash_reports, resText = R.string.crash_message)
public class MainApplication extends MultiDexApplication {
private static MainApplication app;
public static MainApplication getApp() {
return app;
}
@Override
public void onCreate() {
super.onCreate();
app = this;
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(MainApplication.this);
Cyanea.init(this, super.getResources());
List<CyaneaTheme> list = CyaneaTheme.Companion.from(getAssets(), "themes/cyanea_themes.json");
int theme = sharedpreferences.getInt(getString(R.string.SET_THEME), 0);
boolean custom_theme = sharedpreferences.getBoolean("use_custom_theme", false);
if (!custom_theme) {
list.get(theme).apply(Cyanea.getInstance());
}
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
boolean send_crash_reports = sharedpreferences.getBoolean(getString(R.string.SET_SEND_CRASH_REPORTS), false);
if (send_crash_reports) {
CoreConfigurationBuilder ACRABuilder = new CoreConfigurationBuilder(this);
ACRABuilder.setBuildConfigClass(BuildConfig.class).setReportFormat(StringFormat.KEY_VALUE_LIST);
int versionCode = BuildConfig.VERSION_CODE;
ACRABuilder.getPluginConfigurationBuilder(MailSenderConfigurationBuilder.class).setReportAsFile(true).setMailTo("hello@fedilab.app").setSubject("[Fedilab] - Crash Report " + versionCode).setEnabled(true);
ACRABuilder.getPluginConfigurationBuilder(LimiterConfigurationBuilder.class).setEnabled(true);
ACRA.init(this, ACRABuilder);
}
Toasty.Config.getInstance().apply();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(MainApplication.this);
}
}

View file

@ -0,0 +1,129 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.Timeline;
import app.fedilab.android.databinding.ActivityActionsBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
public class ActionActivity extends BaseActivity {
private ActivityActionsBinding binding;
private boolean canGoBack;
private FragmentMastodonTimeline fragmentMastodonTimeline;
private FragmentMastodonAccount fragmentMastodonAccount;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivityActionsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
canGoBack = false;
binding.favourites.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.FAVOURITE_TIMELINE));
binding.bookmarks.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BOOKMARK_TIMELINE));
binding.muted.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.MUTED_TIMELINE));
binding.blocked.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BLOCKED_TIMELINE));
}
private void displayTimeline(Timeline.TimeLineEnum type) {
canGoBack = true;
if (type == Timeline.TimeLineEnum.MUTED_TIMELINE || type == Timeline.TimeLineEnum.BLOCKED_TIMELINE) {
ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> {
fragmentMastodonAccount = new FragmentMastodonAccount();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, type);
fragmentMastodonAccount.setArguments(bundle);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, fragmentMastodonAccount);
fragmentTransaction.commit();
});
} else {
ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> {
fragmentMastodonTimeline = new FragmentMastodonTimeline();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, type);
fragmentMastodonTimeline.setArguments(bundle);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, fragmentMastodonTimeline);
fragmentTransaction.commit();
});
}
switch (type) {
case MUTED_TIMELINE:
setTitle(R.string.muted_menu);
break;
case FAVOURITE_TIMELINE:
setTitle(R.string.favourite);
break;
case BLOCKED_TIMELINE:
setTitle(R.string.blocked_menu);
break;
case BOOKMARK_TIMELINE:
setTitle(R.string.bookmarks);
break;
}
}
@Override
public void onBackPressed() {
if (canGoBack) {
canGoBack = false;
ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.buttonContainer, () -> {
if (fragmentMastodonTimeline != null) {
fragmentMastodonTimeline.onDestroyView();
}
if (fragmentMastodonAccount != null) {
fragmentMastodonAccount.onDestroyView();
}
});
setTitle(R.string.interactions);
} else {
super.onBackPressed();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,45 @@
package app.fedilab.android.activities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.annotation.SuppressLint;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.jaredrummler.cyanea.app.CyaneaAppCompatActivity;
import com.vanniktech.emoji.EmojiManager;
import com.vanniktech.emoji.one.EmojiOneProvider;
import app.fedilab.android.helper.Helper;
@SuppressLint("Registered")
public class BaseActivity extends CyaneaAppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Helper.setLocale(this);
}
static {
Helper.installProvider();
EmojiManager.install(new EmojiOneProvider());
}
}

View file

@ -0,0 +1,36 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.annotation.SuppressLint;
import com.jaredrummler.cyanea.app.CyaneaFragmentActivity;
import com.vanniktech.emoji.EmojiManager;
import com.vanniktech.emoji.one.EmojiOneProvider;
import app.fedilab.android.helper.Helper;
@SuppressLint("Registered")
public class BaseFragmentActivity extends CyaneaFragmentActivity {
static {
Helper.installProvider();
EmojiManager.install(new EmojiOneProvider());
}
}

View file

@ -0,0 +1,599 @@
package app.fedilab.android.activities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.ui.drawer.ComposeAdapter.prepareDraft;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.Account;
import app.fedilab.android.client.entities.StatusDraft;
import app.fedilab.android.client.mastodon.entities.Context;
import app.fedilab.android.client.mastodon.entities.Mention;
import app.fedilab.android.client.mastodon.entities.ScheduledStatus;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.databinding.ActivityPaginationBinding;
import app.fedilab.android.databinding.PopupContactBinding;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.DividerDecorationSimple;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.MediaHelper;
import app.fedilab.android.helper.SpannableHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.jobs.ScheduleThreadWorker;
import app.fedilab.android.services.PostMessageService;
import app.fedilab.android.ui.drawer.AccountsReplyAdapter;
import app.fedilab.android.ui.drawer.ComposeAdapter;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.viewmodel.mastodon.StatusesVM;
import es.dmoral.toasty.Toasty;
public class ComposeActivity extends BaseActivity implements ComposeAdapter.ManageDrafts, AccountsReplyAdapter.ActionDone {
public static final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 754;
public static final int REQUEST_AUDIO_PERMISSION_RESULT = 1653;
public static final int PICK_MEDIA = 5700;
public static final int TAKE_PHOTO = 5600;
private List<Status> statusList;
private Status statusReply, statusMention;
private StatusDraft statusDraft;
private ComposeAdapter composeAdapter;
private ActivityPaginationBinding binding;
private Account account;
private String instance, token;
private Uri photoFileUri;
private ScheduledStatus scheduledStatus;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
binding = ActivityPaginationBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
ActionBar actionBar = getSupportActionBar();
//Remove title
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
}
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
statusList = new ArrayList<>();
Bundle b = getIntent().getExtras();
if (b != null) {
statusReply = (Status) b.getSerializable(Helper.ARG_STATUS_REPLY);
statusDraft = (StatusDraft) b.getSerializable(Helper.ARG_STATUS_DRAFT);
scheduledStatus = (ScheduledStatus) b.getSerializable(Helper.ARG_STATUS_SCHEDULED);
statusMention = (Status) b.getSerializable(Helper.ARG_STATUS_MENTION);
account = (Account) b.getSerializable(Helper.ARG_ACCOUNT);
instance = b.getString(Helper.ARG_INSTANCE, BaseMainActivity.currentInstance);
token = b.getString(Helper.ARG_TOKEN, BaseMainActivity.currentToken);
}
binding.toolbar.setPopupTheme(Helper.popupStyle());
//Edit a scheduled status from server
if (scheduledStatus != null) {
statusDraft = new StatusDraft();
List<Status> statuses = new ArrayList<>();
Status status = new Status();
status.text = scheduledStatus.params.text;
status.in_reply_to_id = scheduledStatus.params.in_reply_to_id;
status.poll = scheduledStatus.params.poll;
if (scheduledStatus.params.media_ids != null && scheduledStatus.params.media_ids.size() > 0) {
status.media_attachments = new ArrayList<>();
new Thread(() -> {
StatusesVM statusesVM = new ViewModelProvider(ComposeActivity.this).get(StatusesVM.class);
for (String attachmentId : scheduledStatus.params.media_ids) {
statusesVM.getAttachment(instance, token, attachmentId)
.observe(ComposeActivity.this, attachment -> status.media_attachments.add(attachment));
}
}).start();
}
status.sensitive = scheduledStatus.params.sensitive;
status.spoiler_text = scheduledStatus.params.spoiler_text;
status.visibility = scheduledStatus.params.visibility;
statusDraft.statusDraftList = statuses;
}
if (instance == null) {
instance = BaseMainActivity.currentInstance;
}
if (token == null) {
token = BaseMainActivity.currentToken;
}
if (account == null) {
account = BaseMainActivity.accountWeakReference.get();
}
StatusesVM statusesVM = new ViewModelProvider(ComposeActivity.this).get(StatusesVM.class);
//Empty compose
List<Status> statusDraftList = new ArrayList<>();
Status status = new Status();
statusDraftList.add(status);
//Restore a draft with all messages
if (statusDraft != null && statusDraft.statusReplyList != null) {
new Thread(() -> {
statusDraft.statusReplyList = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusDraft.statusReplyList);
Handler mainHandler = new Handler(Looper.getMainLooper());
Runnable myRunnable = () -> {
if (statusDraft.statusReplyList != null) {
statusList.addAll(statusDraft.statusReplyList);
binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList));
}
int statusCount = statusList.size();
statusList.addAll(statusDraft.statusDraftList);
composeAdapter = new ComposeAdapter(statusList, statusCount, account);
composeAdapter.manageDrafts = this;
LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this);
binding.recyclerView.setLayoutManager(mLayoutManager);
binding.recyclerView.setAdapter(composeAdapter);
binding.recyclerView.scrollToPosition(composeAdapter.getItemCount() - 1);
};
mainHandler.post(myRunnable);
}).start();
} else if (statusReply != null) {
new Thread(() -> {
statusReply = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusReply);
Handler mainHandler = new Handler(Looper.getMainLooper());
Runnable myRunnable = () -> {
statusList.add(statusReply);
int statusCount = statusList.size();
statusDraftList.get(0).in_reply_to_id = statusReply.id;
statusDraftList.get(0).mentions = statusReply.mentions;
if (statusDraftList.get(0).mentions == null) {
statusDraftList.get(0).mentions = new ArrayList<>();
}
//We will add the mentioned account in mention if not the current user nor if it is already mentioned
if (statusReply.account != null && statusReply.account.acct != null && !statusReply.account.id.equals(BaseMainActivity.currentUserID)) {
boolean canBeAdded = true;
for (Mention mention : statusDraftList.get(0).mentions) {
if (mention.acct.compareToIgnoreCase(statusReply.account.acct) == 0) {
mention.id = null;
canBeAdded = false;
}
}
if (canBeAdded) {
Mention mention = new Mention();
mention.acct = "@" + statusReply.account.acct;
mention.url = statusReply.account.url;
mention.username = statusReply.account.username;
statusDraftList.get(0).mentions.add(mention);
}
}
//StatusDraftList at this point should only have one element
statusList.addAll(statusDraftList);
composeAdapter = new ComposeAdapter(statusList, statusCount, account);
composeAdapter.manageDrafts = this;
LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this);
binding.recyclerView.setLayoutManager(mLayoutManager);
binding.recyclerView.setAdapter(composeAdapter);
statusesVM.getContext(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusReply.id)
.observe(ComposeActivity.this, this::initializeContextView);
};
mainHandler.post(myRunnable);
}).start();
} else {
//Compose without replying
statusList.addAll(statusDraftList);
composeAdapter = new ComposeAdapter(statusList, 0, account);
composeAdapter.manageDrafts = this;
LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this);
binding.recyclerView.setLayoutManager(mLayoutManager);
binding.recyclerView.setAdapter(composeAdapter);
if (statusMention != null) {
composeAdapter.loadMentions(statusMention);
}
}
MastodonHelper.loadPPMastodon(binding.profilePicture, account.mastodon_account);
}
@Override
public void onBackPressed() {
storeDraftWarning();
}
private void storeDraftWarning() {
if (statusDraft == null) {
statusDraft = prepareDraft(statusList, composeAdapter, account.instance, account.user_id);
}
if (canBeSent(statusDraft)) {
AlertDialog.Builder alt_bld = new AlertDialog.Builder(ComposeActivity.this, Helper.dialogStyle());
alt_bld.setMessage(R.string.save_draft);
alt_bld.setPositiveButton(R.string.save, (dialog, id) -> {
dialog.dismiss();
storeDraft(false);
finish();
});
alt_bld.setNegativeButton(R.string.no, (dialog, id) -> {
dialog.dismiss();
finish();
});
AlertDialog alert = alt_bld.create();
alert.show();
} else {
finish();
}
}
/**
* Intialize the common view for the context
*
* @param context {@link Context}
*/
private void initializeContextView(final Context context) {
if (context == null) {
return;
}
//Build the array of statuses
statusList.addAll(0, context.ancestors);
composeAdapter.setStatusCount(context.ancestors.size() + 1);
composeAdapter.notifyItemRangeInserted(0, context.ancestors.size());
if (binding.recyclerView.getItemDecorationCount() > 0) {
for (int i = 0; i < binding.recyclerView.getItemDecorationCount(); i++) {
binding.recyclerView.removeItemDecorationAt(i);
}
}
binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList));
binding.recyclerView.scrollToPosition(composeAdapter.getItemCount() - 1);
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_compose, menu);
return true;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
storeDraftWarning();
return true;
} else if (item.getItemId() == R.id.action_photo_camera) {
photoFileUri = MediaHelper.dispatchTakePictureIntent(ComposeActivity.this);
} else if (item.getItemId() == R.id.action_contacts) {
AlertDialog.Builder builderSingle = new AlertDialog.Builder(ComposeActivity.this, Helper.dialogStyle());
builderSingle.setTitle(getString(R.string.select_accounts));
PopupContactBinding popupContactBinding = PopupContactBinding.inflate(getLayoutInflater(), new LinearLayout(ComposeActivity.this), false);
popupContactBinding.loader.setVisibility(View.VISIBLE);
AccountsVM accountsVM = new ViewModelProvider(ComposeActivity.this).get(AccountsVM.class);
accountsVM.searchAccounts(instance, token, "", 10, false, true)
.observe(ComposeActivity.this, accounts -> onRetrieveContact(popupContactBinding, accounts));
popupContactBinding.searchAccount.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (count > 0) {
popupContactBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_close_24, 0);
} else {
popupContactBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_search_24, 0);
}
}
@Override
public void afterTextChanged(Editable s) {
if (s != null && s.length() > 0) {
accountsVM.searchAccounts(instance, token, s.toString().trim(), 10, false, true)
.observe(ComposeActivity.this, accounts -> onRetrieveContact(popupContactBinding, accounts));
}
}
});
popupContactBinding.searchAccount.setOnTouchListener((v, event) -> {
final int DRAWABLE_RIGHT = 2;
if (event.getAction() == MotionEvent.ACTION_UP) {
if (popupContactBinding.searchAccount.length() > 0 && event.getRawX() >= (popupContactBinding.searchAccount.getRight() - popupContactBinding.searchAccount.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) {
popupContactBinding.searchAccount.setText("");
accountsVM.searchAccounts(instance, token, "", 10, false, true)
.observe(ComposeActivity.this, accounts -> onRetrieveContact(popupContactBinding, accounts));
}
}
return false;
});
builderSingle.setView(popupContactBinding.getRoot());
builderSingle.setNegativeButton(R.string.validate, (dialog, which) -> {
dialog.dismiss();
composeAdapter.putCursor();
});
builderSingle.show();
} else if (item.getItemId() == R.id.action_microphone) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED) {
MediaHelper.recordAudio(ComposeActivity.this, file -> {
List<Uri> uris = new ArrayList<>();
uris.add(Uri.fromFile(new File(file)));
composeAdapter.addAttachment(-1, uris);
});
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
Toast.makeText(this,
getString(R.string.audio), Toast.LENGTH_SHORT).show();
}
requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO
}, REQUEST_AUDIO_PERMISSION_RESULT);
}
} else {
MediaHelper.recordAudio(ComposeActivity.this, file -> {
List<Uri> uris = new ArrayList<>();
uris.add(Uri.fromFile(new File(file)));
composeAdapter.addAttachment(-1, uris);
});
}
} else if (item.getItemId() == R.id.action_schedule) {
if (statusDraft == null) {
statusDraft = prepareDraft(statusList, composeAdapter, account.instance, account.user_id);
}
if (canBeSent(statusDraft)) {
MediaHelper.scheduleMessage(ComposeActivity.this, date -> storeDraft(true, date));
} else {
Toasty.info(ComposeActivity.this, getString(R.string.toot_error_no_content), Toasty.LENGTH_SHORT).show();
}
}
return true;
}
private void onRetrieveContact(PopupContactBinding binding, List<app.fedilab.android.client.mastodon.entities.Account> accounts) {
binding.loader.setVisibility(View.GONE);
if (accounts == null) {
accounts = new ArrayList<>();
}
List<Boolean> checkedValues = new ArrayList<>();
List<app.fedilab.android.client.mastodon.entities.Account> contacts = new ArrayList<>(accounts);
for (app.fedilab.android.client.mastodon.entities.Account account : contacts) {
checkedValues.add(composeAdapter.getLastComposeContent().contains("@" + account.acct));
}
AccountsReplyAdapter contactAdapter = new AccountsReplyAdapter(contacts, checkedValues);
binding.lvAccountsSearch.setAdapter(contactAdapter);
binding.lvAccountsSearch.setLayoutManager(new LinearLayoutManager(ComposeActivity.this));
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
List<Uri> uris = new ArrayList<>();
if (requestCode >= PICK_MEDIA && resultCode == RESULT_OK) {
ClipData clipData = data.getClipData();
int position = requestCode - PICK_MEDIA;
if (clipData != null) {
for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
uris.add(item.getUri());
}
} else {
uris.add(data.getData());
}
composeAdapter.addAttachment(position, uris);
} else if (requestCode == TAKE_PHOTO && resultCode == RESULT_OK) {
uris.add(photoFileUri);
composeAdapter.addAttachment(-1, uris);
}
}
@Override
public void onItemDraftAdded(int position) {
Status status = new Status();
status.mentions = statusList.get(position).mentions;
status.visibility = statusList.get(position).visibility;
status.spoiler_text = statusList.get(position).spoiler_text;
status.sensitive = statusList.get(position).sensitive;
statusList.add(status);
composeAdapter.notifyItemInserted(position + 1);
binding.recyclerView.smoothScrollToPosition(position + 1);
}
@Override
public void onItemDraftDeleted(Status status, int position) {
statusList.remove(status);
composeAdapter.notifyItemRemoved(position);
}
@Override
public void onSubmit(StatusDraft draft) {
//Store in drafts
if (statusDraft == null) {
statusDraft = draft;
} else {
statusDraft.statusDraftList = draft.statusDraftList;
}
storeDraft(true);
}
private void storeDraft(boolean sendMessage) {
storeDraft(sendMessage, null);
}
private void storeDraft(boolean sendMessage, String scheduledDate) {
new Thread(() -> {
//Collect all statusCompose
List<Status> statusDrafts = new ArrayList<>();
List<Status> statusReplies = new ArrayList<>();
for (Status status : statusList) {
if (status.id == null) {
statusDrafts.add(status);
} else {
statusReplies.add(status);
}
}
if (statusDraft == null) {
statusDraft = new StatusDraft(ComposeActivity.this);
} else {
//Draft previously and date is changed
if (statusDraft.scheduled_at != null && scheduledDate != null && statusDraft.workerUuid != null) {
WorkManager.getInstance(ComposeActivity.this).cancelWorkById(statusDraft.workerUuid);
}
}
statusDraft.statusReplyList = statusReplies;
statusDraft.statusDraftList = statusDrafts;
statusDraft.instance = account.instance;
statusDraft.user_id = account.user_id;
if (!canBeSent(statusDraft)) {
return;
}
if (statusDraft.id > 0) {
try {
new StatusDraft(ComposeActivity.this).updateStatusDraft(statusDraft);
} catch (DBException e) {
e.printStackTrace();
}
} else {
try {
statusDraft.id = new StatusDraft(ComposeActivity.this).insertStatusDraft(statusDraft);
} catch (DBException e) {
e.printStackTrace();
}
}
//Only one single message scheduled
if (sendMessage && scheduledDate != null && statusDraft.statusDraftList.size() > 1) {
//Schedule a thread
SimpleDateFormat sdf = new SimpleDateFormat(Helper.SCHEDULE_DATE_FORMAT, Locale.getDefault());
Date date;
try {
date = sdf.parse(scheduledDate);
long delayToPass = 0;
if (date != null) {
delayToPass = (date.getTime() - new Date().getTime());
}
Data inputData = new Data.Builder()
.putString(Helper.ARG_INSTANCE, BaseMainActivity.currentInstance)
.putString(Helper.ARG_TOKEN, BaseMainActivity.currentToken)
.putString(Helper.ARG_USER_ID, BaseMainActivity.currentUserID)
.putLong(Helper.ARG_STATUS_DRAFT_ID, statusDraft.id)
.build();
OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(ScheduleThreadWorker.class)
.setInputData(inputData)
.addTag(Helper.WORKER_SCHEDULED_STATUSES)
.setInitialDelay(delayToPass, TimeUnit.MILLISECONDS)
.build();
statusDraft.workerUuid = oneTimeWorkRequest.getId();
statusDraft.scheduled_at = date;
Handler mainHandler = new Handler(Looper.getMainLooper());
Runnable myRunnable = () -> {
Toasty.info(ComposeActivity.this, getString(R.string.toot_scheduled), Toasty.LENGTH_LONG).show();
finish();
};
mainHandler.post(myRunnable);
} catch (ParseException e) {
e.printStackTrace();
}
} else if (sendMessage) {
Intent intent = new Intent(ComposeActivity.this, PostMessageService.class);
intent.putExtra(Helper.ARG_STATUS_DRAFT, statusDraft);
intent.putExtra(Helper.ARG_INSTANCE, instance);
intent.putExtra(Helper.ARG_TOKEN, token);
intent.putExtra(Helper.ARG_SCHEDULED_DATE, scheduledDate);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}
finish();
}
}).start();
}
private boolean canBeSent(StatusDraft statusDraft) {
if (statusDraft == null || statusDraft.statusDraftList == null || statusDraft.statusDraftList.size() == 0) {
return false;
}
Status statusCheck = statusDraft.statusDraftList.get(0);
if (statusCheck == null) {
return false;
}
return (statusCheck.text != null && statusCheck.text.trim().length() != 0)
|| (statusCheck.media_attachments != null && statusCheck.media_attachments.size() != 0)
|| statusCheck.poll != null
|| (statusCheck.spoiler_text != null && statusCheck.spoiler_text.trim().length() != 0);
}
@Override
public void onContactClick(boolean isChecked, String acct) {
composeAdapter.updateContent(isChecked, acct);
}
public enum mediaType {
PHOTO,
VIDEO,
AUDIO,
ALL
}
}

View file

@ -0,0 +1,143 @@
package app.fedilab.android.activities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.databinding.ActivityConversationBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.SpannableHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonContext;
public class ContextActivity extends BaseActivity {
public static boolean expand;
public static boolean displayCW;
Fragment currentFragment;
private Status focusedStatus;
private ActivityConversationBinding binding;
public static Resources.Theme theme;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
binding = ActivityConversationBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
ActionBar actionBar = getSupportActionBar();
//Remove title
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
}
binding.title.setText(R.string.context_conversation);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
binding.toolbar.setPopupTheme(Helper.popupStyle());
Bundle b = getIntent().getExtras();
final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ContextActivity.this);
displayCW = sharedpreferences.getBoolean(getString(R.string.SET_EXPAND_CW), false);
focusedStatus = null; // or other values
if (b != null)
focusedStatus = (Status) b.getSerializable(Helper.ARG_STATUS);
if (focusedStatus == null) {
finish();
return;
}
MastodonHelper.loadPPMastodon(binding.profilePicture, BaseMainActivity.accountWeakReference.get().mastodon_account);
Bundle bundle = new Bundle();
new Thread(() -> {
focusedStatus = SpannableHelper.convertStatus(getApplication().getApplicationContext(), focusedStatus);
Handler mainHandler = new Handler(Looper.getMainLooper());
Runnable myRunnable = () -> {
bundle.putSerializable(Helper.ARG_STATUS, focusedStatus);
currentFragment = Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_content_main, new FragmentMastodonContext(), bundle, null, null);
};
mainHandler.post(myRunnable);
}).start();
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_context, menu);
MenuItem itemExpand = menu.findItem(R.id.action_expand);
if (expand) {
itemExpand.setIcon(R.drawable.ic_baseline_expand_less_24);
} else {
itemExpand.setIcon(R.drawable.ic_baseline_expand_more_24);
}
MenuItem itemDisplayCW = menu.findItem(R.id.action_show_cw);
if (displayCW) {
itemDisplayCW.setIcon(R.drawable.ic_baseline_remove_red_eye_24);
} else {
itemDisplayCW.setIcon(R.drawable.ic_outline_remove_red_eye_24);
}
return true;
}
public void setCurrentFragment(FragmentMastodonContext fragmentMastodonContext) {
currentFragment = fragmentMastodonContext;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.action_expand) {
expand = !expand;
if (currentFragment != null && currentFragment instanceof FragmentMastodonContext) {
((FragmentMastodonContext) currentFragment).redraw();
}
invalidateOptionsMenu();
} else if (item.getItemId() == R.id.action_show_cw) {
displayCW = !displayCW;
if (currentFragment != null && currentFragment instanceof FragmentMastodonContext) {
((FragmentMastodonContext) currentFragment).refresh();
}
invalidateOptionsMenu();
}
return true;
}
@Override
protected void onDestroy() {
super.onDestroy();
binding = null;
currentFragment = null;
}
}

View file

@ -0,0 +1,250 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.TextUtils;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.preference.PreferenceManager;
import java.util.List;
import java.util.Set;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.mastodon.entities.Attachment;
import app.fedilab.android.client.mastodon.entities.Emoji;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.client.mastodon.entities.Tag;
import app.fedilab.android.databinding.ActivityCustomSharingBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.helper.customsharing.CustomSharingAsyncTask;
import app.fedilab.android.helper.customsharing.CustomSharingResponse;
import app.fedilab.android.helper.customsharing.OnCustomSharingInterface;
import es.dmoral.toasty.Toasty;
/**
* Created by Curtis on 13/02/2019.
* Share status metadata to remote content aggregators
*/
public class CustomSharingActivity extends BaseActivity implements OnCustomSharingInterface {
private String title, keywords, custom_sharing_url, encodedCustomSharingURL;
private String bundle_url;
private String bundle_source;
private String bundle_id;
private String bundle_content;
private String bundle_thumbnailurl;
private String bundle_creator;
private ActivityCustomSharingBinding binding;
private Status status;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(CustomSharingActivity.this);
binding = ActivityCustomSharingBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
Bundle b = getIntent().getExtras();
status = null;
if (b != null) {
status = (Status) b.getSerializable(Helper.ARG_STATUS);
}
if (status == null) {
finish();
return;
}
bundle_creator = status.account.acct;
bundle_url = status.url;
bundle_id = status.uri;
bundle_source = status.account.url;
String bundle_tags = getTagsString();
bundle_content = formatedContent(status.content, status.emojis);
if (status.card != null && status.card.image != null) {
bundle_thumbnailurl = status.card.image;
} else if (status.media_attachments != null && status.media_attachments.size() > 0) {
List<Attachment> mediaAttachments = status.media_attachments;
Attachment firstAttachment = mediaAttachments.get(0);
bundle_thumbnailurl = firstAttachment.preview_url;
} else {
bundle_thumbnailurl = status.account.avatar;
}
if (!bundle_creator.contains("@")) {
bundle_creator = bundle_creator + "@" + BaseMainActivity.accountWeakReference.get().instance;
}
binding.setCustomSharingTitle.setEllipsize(TextUtils.TruncateAt.END);
//set text on title, description, and keywords
String[] lines = bundle_content.split("\n");
//Remove tags in title
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
lines[0] = Html.fromHtml(lines[0], Html.FROM_HTML_MODE_LEGACY).toString();
else
lines[0] = Html.fromHtml(lines[0]).toString();
String newTitle;
if (lines[0].length() > 60) {
newTitle = lines[0].substring(0, 60) + '…';
} else {
newTitle = lines[0];
}
binding.setCustomSharingTitle.setText(newTitle);
String newDescription;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
newDescription = Html.fromHtml(bundle_content, Html.FROM_HTML_MODE_LEGACY).toString();
else
newDescription = Html.fromHtml(bundle_content).toString();
binding.setCustomSharingDescription.setText(newDescription);
binding.setCustomSharingKeywords.setText(bundle_tags);
binding.setCustomSharingSave.setOnClickListener(v -> {
// obtain title, description, keywords
title = binding.setCustomSharingTitle.getText().toString();
keywords = binding.setCustomSharingKeywords.getText().toString();
CharSequence comma_only = ",";
CharSequence space_only = " ";
CharSequence double_space = " ";
keywords = keywords.replace(comma_only, space_only);
keywords = keywords.replace(double_space, space_only);
// Create encodedCustomSharingURL
custom_sharing_url = sharedpreferences.getString(getString(R.string.SET_CUSTOM_SHARING_URL),
"http://example.net/add?token=YOUR_TOKEN&url=${url}&title=${title}" +
"&source=${source}&id=${id}&description=${description}&keywords=${keywords}&creator=${creator}&thumbnailurl=${thumbnailurl}");
encodedCustomSharingURL = encodeCustomSharingURL();
new CustomSharingAsyncTask(CustomSharingActivity.this, encodedCustomSharingURL, CustomSharingActivity.this);
});
}
private String getTagsString() {
//iterate through tags and create comma delimited string of tag names
StringBuilder tag_names = new StringBuilder();
for (Tag t : status.tags) {
if (tag_names.toString().equals("")) {
tag_names = new StringBuilder(t.name);
} else {
tag_names.append(", ").append(t.name);
}
}
return tag_names.toString();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onCustomSharing(CustomSharingResponse customSharingResponse) {
binding.setCustomSharingSave.setEnabled(true);
if (customSharingResponse.getError() != null) {
Toasty.error(CustomSharingActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show();
return;
}
String response = customSharingResponse.getResponse();
Toasty.success(CustomSharingActivity.this, response, Toast.LENGTH_LONG).show();
finish();
}
public String encodeCustomSharingURL() {
Uri uri = Uri.parse(custom_sharing_url);
String protocol = uri.getScheme();
String server = uri.getAuthority();
String path = uri.getPath();
if (path != null) {
path = path.replaceAll("/", "");
}
Uri.Builder builder = new Uri.Builder();
builder.scheme(protocol)
.authority(server)
.appendPath(path);
Set<String> args = uri.getQueryParameterNames();
boolean paramFound;
for (String param_name : args) {
paramFound = false;
String param_value = uri.getQueryParameter(param_name);
if (param_value != null)
switch (param_value) {
case "${url}":
paramFound = true;
builder.appendQueryParameter(param_name, bundle_url);
break;
case "${title}":
paramFound = true;
builder.appendQueryParameter(param_name, title);
break;
case "${source}":
paramFound = true;
builder.appendQueryParameter(param_name, bundle_source);
break;
case "${id}":
paramFound = true;
builder.appendQueryParameter(param_name, bundle_id);
break;
case "${description}":
paramFound = true;
builder.appendQueryParameter(param_name, bundle_content);
break;
case "${keywords}":
paramFound = true;
builder.appendQueryParameter(param_name, keywords);
break;
case "${creator}":
paramFound = true;
builder.appendQueryParameter(param_name, bundle_creator);
break;
case "${thumbnailurl}":
paramFound = true;
builder.appendQueryParameter(param_name, bundle_thumbnailurl);
break;
}
if (!paramFound) {
builder.appendQueryParameter(param_name, param_value);
}
}
return builder.build().toString();
}
private String formatedContent(String content, List<Emoji> emojis) {
//Avoid null content
if (content == null)
return "";
if (emojis == null || emojis.size() == 0)
return content;
for (Emoji emoji : emojis) {
content = content.replaceAll(":" + emoji.shortcode + ":", "<img src='" + emoji.url + "' width=20 alt='" + emoji.shortcode + "'/>");
}
return content;
}
}

View file

@ -0,0 +1,213 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.StatusDraft;
import app.fedilab.android.client.mastodon.entities.Attachment;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.databinding.ActivityDraftsBinding;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.StatusDraftAdapter;
import app.fedilab.android.viewmodel.mastodon.TimelinesVM;
public class DraftActivity extends BaseActivity implements StatusDraftAdapter.DraftActions {
private ActivityDraftsBinding binding;
private List<StatusDraft> statusDrafts;
private StatusDraftAdapter statusDraftAdapter;
private TimelinesVM timelinesVM;
private LinearLayoutManager mLayoutManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
binding = ActivityDraftsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
ActionBar actionBar = getSupportActionBar();
//Remove title
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
}
binding.toolbar.setPopupTheme(Helper.popupStyle());
binding.title.setText(R.string.drafts);
binding.loader.setVisibility(View.VISIBLE);
binding.lvStatus.setVisibility(View.GONE);
binding.noAction.setVisibility(View.GONE);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
timelinesVM = new ViewModelProvider(DraftActivity.this).get(TimelinesVM.class);
timelinesVM.getDrafts(BaseMainActivity.accountWeakReference.get())
.observe(DraftActivity.this, this::initializeDraftView);
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_draft, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.action_delete) {
AlertDialog.Builder unfollowConfirm = new AlertDialog.Builder(DraftActivity.this, Helper.dialogStyle());
unfollowConfirm.setTitle(getString(R.string.delete_all));
unfollowConfirm.setMessage(getString(R.string.remove_draft));
unfollowConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
unfollowConfirm.setPositiveButton(R.string.delete, (dialog, which) -> {
new Thread(() -> {
if (statusDrafts != null) {
for (StatusDraft statusDraft : statusDrafts) {
//Check if there are media in the drafts
List<Attachment> attachments = new ArrayList<>();
if (statusDraft.statusDraftList != null) {
for (Status drafts : statusDraft.statusDraftList) {
if (drafts.media_attachments != null && drafts.media_attachments.size() > 0) {
attachments.addAll(drafts.media_attachments);
}
}
}
//If there are media, we need to remove them first.
if (attachments.size() > 0) {
for (Attachment attachment : attachments) {
if (attachment.local_path != null) {
File fileToDelete = new File(attachment.local_path);
if (fileToDelete.exists()) {
//noinspection ResultOfMethodCallIgnored
fileToDelete.delete();
}
}
}
}
}
try {
//Delete the draft
new StatusDraft(DraftActivity.this).removeAllDraft();
Handler mainHandler = new Handler(Looper.getMainLooper());
Runnable myRunnable = () -> statusDraftAdapter.draftActions.onAllDeleted();
mainHandler.post(myRunnable);
} catch (DBException e) {
e.printStackTrace();
}
}
}).start();
dialog.dismiss();
});
unfollowConfirm.show();
return true;
}
return true;
}
/**
* Intialize the view for drafts
*
* @param statusDrafts {@link List<StatusDraft>}
*/
private void initializeDraftView(List<StatusDraft> statusDrafts) {
if (statusDrafts == null) {
statusDrafts = new ArrayList<>();
}
binding.loader.setVisibility(View.GONE);
if (statusDrafts.size() > 0) {
binding.lvStatus.setVisibility(View.VISIBLE);
this.statusDrafts = statusDrafts;
statusDraftAdapter = new StatusDraftAdapter(this.statusDrafts);
statusDraftAdapter.draftActions = this;
mLayoutManager = new LinearLayoutManager(DraftActivity.this);
binding.lvStatus.setLayoutManager(mLayoutManager);
binding.lvStatus.setAdapter(statusDraftAdapter);
} else {
binding.noAction.setVisibility(View.VISIBLE);
}
}
@Override
protected void onResume() {
super.onResume();
//We need to check if drafts changed (ie when coming back from the compose activity)
if (statusDrafts != null && timelinesVM != null) {
timelinesVM.getDrafts(BaseMainActivity.accountWeakReference.get())
.observe(DraftActivity.this, this::updateDrafts);
}
}
private void updateDrafts(List<StatusDraft> statusDrafts) {
if (statusDrafts == null) {
statusDrafts = new ArrayList<>();
}
int currentPosition = mLayoutManager.findFirstVisibleItemPosition();
if (statusDrafts.size() > 0) {
int count = this.statusDrafts.size();
this.statusDrafts.clear();
this.statusDrafts = new ArrayList<>();
statusDraftAdapter.notifyItemRangeRemoved(0, count);
this.statusDrafts = statusDrafts;
statusDraftAdapter = new StatusDraftAdapter(this.statusDrafts);
statusDraftAdapter.draftActions = this;
mLayoutManager = new LinearLayoutManager(DraftActivity.this);
binding.lvStatus.setLayoutManager(mLayoutManager);
binding.lvStatus.setAdapter(statusDraftAdapter);
if (currentPosition < this.statusDrafts.size()) {
binding.lvStatus.scrollToPosition(currentPosition);
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
binding.lvStatus.setAdapter(null);
binding = null;
}
@Override
public void onAllDeleted() {
binding.lvStatus.setVisibility(View.GONE);
binding.noAction.setVisibility(View.VISIBLE);
}
}

View file

@ -0,0 +1,291 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.BaseMainActivity.instanceInfo;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.Html;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.bumptech.glide.Glide;
import com.google.android.material.textfield.TextInputEditText;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.client.mastodon.entities.Field;
import app.fedilab.android.databinding.AccountFieldItemBinding;
import app.fedilab.android.databinding.ActivityEditProfileBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import es.dmoral.toasty.Toasty;
public class EditProfileActivity extends BaseActivity {
public static final int PICK_MEDIA_AVATAR = 5705;
public static final int PICK_MEDIA_HEADER = 5706;
private ActivityEditProfileBinding binding;
private AccountsVM accountsVM;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivityEditProfileBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
new ViewModelProvider(EditProfileActivity.this).get(AccountsVM.class).getConnectedAccount(BaseMainActivity.currentInstance, BaseMainActivity.currentToken)
.observe(EditProfileActivity.this, account -> {
BaseMainActivity.accountWeakReference.get().mastodon_account = account;
initializeView();
});
}
@SuppressWarnings("deprecation")
private void initializeView() {
//Hydrate values
MastodonHelper.loadProfileMediaMastodon(binding.bannerPp, BaseMainActivity.accountWeakReference.get().mastodon_account, MastodonHelper.MediaAccountType.HEADER);
MastodonHelper.loadPPMastodon(binding.accountPp, BaseMainActivity.accountWeakReference.get().mastodon_account);
binding.displayName.setText(BaseMainActivity.accountWeakReference.get().mastodon_account.display_name);
binding.acct.setText(String.format(Locale.getDefault(), "%s@%s", BaseMainActivity.accountWeakReference.get().mastodon_account.acct, BaseMainActivity.currentInstance));
String bio;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
bio = Html.fromHtml(BaseMainActivity.accountWeakReference.get().mastodon_account.note, Html.FROM_HTML_MODE_LEGACY).toString();
else
bio = Html.fromHtml(BaseMainActivity.accountWeakReference.get().mastodon_account.note).toString();
binding.bio.setText(bio);
binding.sensitive.setChecked(BaseMainActivity.accountWeakReference.get().mastodon_account.source.sensitive);
binding.bot.setChecked(BaseMainActivity.accountWeakReference.get().mastodon_account.bot);
binding.discoverable.setChecked(BaseMainActivity.accountWeakReference.get().mastodon_account.discoverable);
switch (BaseMainActivity.accountWeakReference.get().mastodon_account.source.privacy) {
case "public":
binding.visibilityPublic.setChecked(true);
break;
case "unlisted":
binding.visibilityUnlisted.setChecked(true);
break;
case "private":
binding.visibilityPrivate.setChecked(true);
break;
case "direct":
binding.visibilityDirect.setChecked(true);
break;
}
if (BaseMainActivity.accountWeakReference.get().mastodon_account.locked) {
binding.locked.setChecked(true);
} else {
binding.unlocked.setChecked(true);
}
List<Field> fields = BaseMainActivity.accountWeakReference.get().mastodon_account.fields;
if (fields != null && fields.size() > 0) {
for (Field field : fields) {
AccountFieldItemBinding fieldItemBinding = AccountFieldItemBinding.inflate(getLayoutInflater());
fieldItemBinding.name.setText(field.name);
String value;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
value = Html.fromHtml(field.value, Html.FROM_HTML_MODE_LEGACY).toString();
else
value = Html.fromHtml(field.value).toString();
fieldItemBinding.value.setText(value);
fieldItemBinding.remove.setOnClickListener(v -> {
AlertDialog.Builder deleteConfirm = new AlertDialog.Builder(EditProfileActivity.this, Helper.dialogStyle());
deleteConfirm.setTitle(getString(R.string.delete_field));
deleteConfirm.setMessage(getString(R.string.delete_field_confirm));
deleteConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
deleteConfirm.setPositiveButton(R.string.delete, (dialog, which) -> {
binding.fieldsContainer.removeView(fieldItemBinding.getRoot());
if (binding.fieldsContainer.getChildCount() < 4) {
binding.fieldsContainer.setVisibility(View.VISIBLE);
} else {
binding.fieldsContainer.setVisibility(View.GONE);
}
dialog.dismiss();
});
deleteConfirm.create().show();
});
binding.fieldsContainer.addView(fieldItemBinding.getRoot());
}
}
binding.addField.setOnClickListener(view -> {
AccountFieldItemBinding fieldItemBinding = AccountFieldItemBinding.inflate(getLayoutInflater());
fieldItemBinding.remove.setOnClickListener(v -> {
AlertDialog.Builder deleteConfirm = new AlertDialog.Builder(EditProfileActivity.this, Helper.dialogStyle());
deleteConfirm.setTitle(getString(R.string.delete_field));
deleteConfirm.setMessage(getString(R.string.delete_field_confirm));
deleteConfirm.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
deleteConfirm.setPositiveButton(R.string.delete, (dialog, which) -> {
binding.fieldsContainer.removeView(fieldItemBinding.getRoot());
if (binding.fieldsContainer.getChildCount() < 4) {
binding.fieldsContainer.setVisibility(View.VISIBLE);
} else {
binding.fieldsContainer.setVisibility(View.GONE);
}
dialog.dismiss();
});
deleteConfirm.create().show();
});
binding.fieldsContainer.addView(fieldItemBinding.getRoot());
if (binding.fieldsContainer.getChildCount() >= 4) {
binding.addField.setVisibility(View.GONE);
}
});
//Actions with the activity
accountsVM = new ViewModelProvider(EditProfileActivity.this).get(AccountsVM.class);
binding.headerSelect.setOnClickListener(view -> startActivityForResult(prepareIntent(), EditProfileActivity.PICK_MEDIA_AVATAR));
binding.avatarSelect.setOnClickListener(view -> startActivityForResult(prepareIntent(), EditProfileActivity.PICK_MEDIA_HEADER));
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PICK_MEDIA_AVATAR && resultCode == RESULT_OK) {
binding.avatarProgress.setVisibility(View.VISIBLE);
Glide.with(EditProfileActivity.this)
.asDrawable()
.load(data.getData())
.thumbnail(0.1f)
.into(binding.accountPp);
accountsVM.updateProfilePicture(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, data.getData(), AccountsVM.UpdateMediaType.AVATAR)
.observe(EditProfileActivity.this, account -> {
sendBroadCast(account);
binding.avatarProgress.setVisibility(View.GONE);
BaseMainActivity.accountWeakReference.get().mastodon_account = account;
});
} else if (requestCode == PICK_MEDIA_HEADER && resultCode == RESULT_OK) {
Glide.with(EditProfileActivity.this)
.asDrawable()
.load(data.getData())
.thumbnail(0.1f)
.into(binding.bannerPp);
binding.headerProgress.setVisibility(View.VISIBLE);
accountsVM.updateProfilePicture(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, data.getData(), AccountsVM.UpdateMediaType.HEADER)
.observe(EditProfileActivity.this, account -> {
sendBroadCast(account);
binding.headerProgress.setVisibility(View.GONE);
BaseMainActivity.accountWeakReference.get().mastodon_account = account;
});
}
}
private void sendBroadCast(Account account) {
Bundle b = new Bundle();
b.putBoolean(Helper.RECEIVE_REDRAW_PROFILE, true);
b.putSerializable(Helper.ARG_ACCOUNT, account);
Intent intentBD = new Intent(Helper.BROADCAST_DATA);
intentBD.putExtras(b);
LocalBroadcastManager.getInstance(EditProfileActivity.this).sendBroadcast(intentBD);
}
private Intent prepareIntent() {
Intent intent;
intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
String[] mimetypes;
long max_size = -1;
if (instanceInfo.getMimeTypeImage().size() > 0) {
mimetypes = instanceInfo.getMimeTypeImage().toArray(new String[0]);
max_size = instanceInfo.configuration.media_attachments.image_size_limit;
} else {
mimetypes = new String[]{"image/*"};
}
if (max_size > 0) {
intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, max_size);
}
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes);
return intent;
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
getMenuInflater().inflate(R.menu.menu_edit_profile, menu);
return super.onCreateOptionsMenu(menu);
}
private String getPrivacy() {
if (binding.visibilityPublic.isChecked()) {
return "public";
} else if (binding.visibilityUnlisted.isChecked()) {
return "unlisted";
} else if (binding.visibilityPrivate.isChecked()) {
return "private";
} else if (binding.visibilityDirect.isChecked()) {
return "direct";
}
return null;
}
LinkedHashMap<Integer, Field.FieldParams> getFields() {
LinkedHashMap<Integer, Field.FieldParams> fields = new LinkedHashMap<>();
for (int i = 0; i < binding.fieldsContainer.getChildCount(); i++) {
Field.FieldParams field = new Field.FieldParams();
field.name = ((TextInputEditText) binding.fieldsContainer.getChildAt(i).findViewById(R.id.name)).getText().toString().trim();
field.value = ((TextInputEditText) binding.fieldsContainer.getChildAt(i).findViewById(R.id.value)).getText().toString().trim();
fields.put(i, field);
}
return fields;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.action_save) {
accountsVM.updateCredentials(BaseMainActivity.currentInstance, BaseMainActivity.currentToken,
binding.discoverable.isChecked(),
binding.bot.isChecked(),
binding.displayName.getText().toString().trim(),
binding.bio.getText().toString(),
binding.locked.isChecked(),
getPrivacy(),
binding.sensitive.isChecked(),
null,
getFields()
)
.observe(EditProfileActivity.this, account -> {
BaseMainActivity.accountWeakReference.get().mastodon_account = account;
sendBroadCast(account);
Toasty.success(EditProfileActivity.this, getString(R.string.profiled_updated), Toasty.LENGTH_LONG).show();
finish();
});
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,238 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.mastodon.entities.Filter;
import app.fedilab.android.databinding.ActivityFiltersBinding;
import app.fedilab.android.databinding.PopupAddFilterBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.FilterAdapter;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
public class FilterActivity extends BaseActivity implements FilterAdapter.Delete {
private ActivityFiltersBinding binding;
private List<Filter> filterList;
private FilterAdapter filterAdapter;
/**
* Method that allows to add or edit filter depending if Filter passing into params is null (null = insertion)
*
* @param context - Context
* @param filter - {@link Filter}
* @param listener - {@link FilterAdapter.FilterAction}
*/
public static void addEditFilter(Context context, Filter filter, FilterAdapter.FilterAction listener) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle());
PopupAddFilterBinding popupAddFilterBinding = PopupAddFilterBinding.inflate(LayoutInflater.from(context));
AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class);
dialogBuilder.setView(popupAddFilterBinding.getRoot());
ArrayAdapter<CharSequence> adapterResize = ArrayAdapter.createFromResource(Objects.requireNonNull(context),
R.array.filter_expire, android.R.layout.simple_spinner_dropdown_item);
popupAddFilterBinding.filterExpire.setAdapter(adapterResize);
final int[] expire = {-1};
popupAddFilterBinding.filterExpire.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent1, View view, int position1, long id) {
switch (position1) {
case 0:
expire[0] = -1;
break;
case 1:
expire[0] = 3600;
break;
case 2:
expire[0] = 21600;
break;
case 3:
expire[0] = 43200;
break;
case 4:
expire[0] = 86400;
break;
case 5:
expire[0] = 604800;
break;
}
}
@Override
public void onNothingSelected(AdapterView<?> parent1) {
}
});
if (filter != null) {
popupAddFilterBinding.addPhrase.setText(filter.phrase);
if (filter.context != null)
for (String val : filter.context) {
switch (val) {
case "home":
popupAddFilterBinding.contextHome.setChecked(true);
break;
case "public":
popupAddFilterBinding.contextPublic.setChecked(true);
break;
case "notifications":
popupAddFilterBinding.contextNotification.setChecked(true);
break;
case "thread":
popupAddFilterBinding.contextConversation.setChecked(true);
break;
}
}
popupAddFilterBinding.contextWholeWord.setChecked(filter.whole_word);
popupAddFilterBinding.contextDrop.setChecked(filter.irreversible);
}
AlertDialog alertDialog = dialogBuilder.setPositiveButton(R.string.validate, null)
.setNegativeButton(R.string.cancel, null).create();
alertDialog.setOnShowListener(dialogInterface -> {
Button button = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(view -> {
if (popupAddFilterBinding.addPhrase.getText() == null || popupAddFilterBinding.addPhrase.getText().toString().trim().length() == 0) {
popupAddFilterBinding.addPhrase.setError(context.getString(R.string.cannot_be_empty));
return;
}
if (!popupAddFilterBinding.contextConversation.isChecked() && !popupAddFilterBinding.contextHome.isChecked() && !popupAddFilterBinding.contextPublic.isChecked() && !popupAddFilterBinding.contextNotification.isChecked()) {
popupAddFilterBinding.contextDescription.setError(context.getString(R.string.cannot_be_empty));
return;
}
if (popupAddFilterBinding.addPhrase.getText() != null && popupAddFilterBinding.addPhrase.getText().toString().trim().length() > 0) {
Filter filterSent = new Filter();
ArrayList<String> contextFilter = new ArrayList<>();
if (popupAddFilterBinding.contextHome.isChecked())
contextFilter.add("home");
if (popupAddFilterBinding.contextPublic.isChecked())
contextFilter.add("public");
if (popupAddFilterBinding.contextNotification.isChecked())
contextFilter.add("notifications");
if (popupAddFilterBinding.contextConversation.isChecked())
contextFilter.add("thread");
filterSent.context = contextFilter;
filterSent.expires_at_sent = expire[0];
filterSent.phrase = popupAddFilterBinding.addPhrase.getText().toString();
filterSent.whole_word = popupAddFilterBinding.contextWholeWord.isChecked();
filterSent.irreversible = popupAddFilterBinding.contextDrop.isChecked();
if (filter != null) {
accountsVM.editFilter(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, filter.id, filterSent.phrase, filterSent.context, filterSent.irreversible, filterSent.whole_word, filterSent.expires_at_sent)
.observe((LifecycleOwner) context, listener::callback);
} else {
accountsVM.addFilter(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, filterSent.phrase, filterSent.context, filterSent.irreversible, filterSent.whole_word, filterSent.expires_at_sent)
.observe((LifecycleOwner) context, listener::callback);
}
alertDialog.dismiss();
}
});
Button buttonCancel = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE);
buttonCancel.setOnClickListener(view -> alertDialog.dismiss());
});
alertDialog.setTitle(context.getString(R.string.action_update_filter));
alertDialog.setOnDismissListener(dialogInterface -> {
//Hide keyboard
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(popupAddFilterBinding.addPhrase.getWindowToken(), 0);
});
if (alertDialog.getWindow() != null) {
alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
alertDialog.show();
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivityFiltersBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
filterList = new ArrayList<>();
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
AccountsVM accountsVM = new ViewModelProvider(FilterActivity.this).get(AccountsVM.class);
accountsVM.getFilters(BaseMainActivity.currentInstance, BaseMainActivity.currentToken)
.observe(FilterActivity.this, filters -> {
BaseMainActivity.mainFilters = filters;
if (filters != null && filters.size() > 0) {
filterList.addAll(filters);
filterAdapter = new FilterAdapter(filterList);
filterAdapter.delete = this;
binding.lvFilters.setAdapter(filterAdapter);
binding.lvFilters.setLayoutManager(new LinearLayoutManager(FilterActivity.this));
} else {
binding.lvFilters.setVisibility(View.GONE);
binding.noAction.setVisibility(View.VISIBLE);
}
});
binding.addFilter.setOnClickListener(v -> addEditFilter(FilterActivity.this, null, filter -> {
if (filter != null) {
filterList.add(0, filter);
if (filterAdapter != null) {
filterAdapter.notifyItemInserted(0);
} else {
filterAdapter = new FilterAdapter(filterList);
filterAdapter.delete = FilterActivity.this;
binding.lvFilters.setAdapter(filterAdapter);
binding.lvFilters.setLayoutManager(new LinearLayoutManager(FilterActivity.this));
}
binding.lvFilters.setVisibility(View.VISIBLE);
binding.noAction.setVisibility(View.GONE);
}
}));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void allFiltersDeleted() {
binding.lvFilters.setVisibility(View.GONE);
binding.noAction.setVisibility(View.VISIBLE);
}
}

View file

@ -0,0 +1,162 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.Pinned;
import app.fedilab.android.client.entities.StatusDraft;
import app.fedilab.android.client.entities.Timeline;
import app.fedilab.android.client.entities.app.PinnedTimeline;
import app.fedilab.android.client.entities.app.TagTimeline;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.databinding.ActivityHashtagBinding;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
import es.dmoral.toasty.Toasty;
public class HashTagActivity extends BaseActivity {
public static int position;
private String tag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
ActivityHashtagBinding binding = ActivityHashtagBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Bundle b = getIntent().getExtras();
if (b != null) {
tag = b.getString(Helper.ARG_SEARCH_KEYWORD, null);
}
if (tag == null)
finish();
setSupportActionBar(binding.toolbar);
ActionBar actionBar = getSupportActionBar();
//Remove title
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
}
binding.title.setText(tag);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TAG);
bundle.putString(Helper.ARG_SEARCH_KEYWORD, tag);
Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_tags, new FragmentMastodonTimeline(), bundle, null, null);
binding.toolbar.setPopupTheme(Helper.popupStyle());
binding.compose.setOnClickListener(v -> {
Intent intentToot = new Intent(HashTagActivity.this, ComposeActivity.class);
StatusDraft statusDraft = new StatusDraft();
Status status = new Status();
status.text = "#" + tag;
List<Status> statuses = new ArrayList<>();
statuses.add(status);
statusDraft.statusDraftList = statuses;
Bundle _b = new Bundle();
_b.putSerializable(Helper.ARG_TAG_TIMELINE, statusDraft);
intentToot.putExtras(_b);
startActivity(intentToot);
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.action_add_timeline) {
new Thread(() -> {
try {
Pinned pinned = new Pinned(HashTagActivity.this).getPinned(BaseMainActivity.accountWeakReference.get());
boolean canBeAdded = true;
boolean update = true;
if (pinned == null) {
pinned = new Pinned();
pinned.pinnedTimelines = new ArrayList<>();
update = false;
} else {
for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) {
if (pinnedTimeline.type == Timeline.TimeLineEnum.TAG) {
if (pinnedTimeline.tagTimeline.name.compareTo(tag.trim()) == 0) {
canBeAdded = false;
}
}
}
}
if (!canBeAdded) {
Toasty.warning(HashTagActivity.this, getString(R.string.tags_already_stored), Toasty.LENGTH_SHORT).show();
return;
}
PinnedTimeline pinnedTimeline = new PinnedTimeline();
pinnedTimeline.type = Timeline.TimeLineEnum.TAG;
pinnedTimeline.position = pinned.pinnedTimelines.size();
pinnedTimeline.displayed = true;
TagTimeline tagTimeline = new TagTimeline();
tagTimeline.name = tag.trim();
tagTimeline.isNSFW = false;
tagTimeline.isART = false;
pinnedTimeline.tagTimeline = tagTimeline;
pinned.pinnedTimelines.add(pinnedTimeline);
if (update) {
new Pinned(HashTagActivity.this).updatePinned(pinned);
} else {
new Pinned(HashTagActivity.this).insertPinned(pinned);
}
Bundle b = new Bundle();
b.putBoolean(Helper.RECEIVE_REDRAW_TOPBAR, true);
Intent intentBD = new Intent(Helper.BROADCAST_DATA);
intentBD.putExtras(b);
LocalBroadcastManager.getInstance(HashTagActivity.this).sendBroadcast(intentBD);
} catch (DBException e) {
e.printStackTrace();
}
}).start();
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
getMenuInflater().inflate(R.menu.menu_reorder, menu);
return super.onCreateOptionsMenu(menu);
}
}

View file

@ -0,0 +1,109 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.lifecycle.ViewModelProvider;
import com.bumptech.glide.Glide;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.mastodon.entities.Instance;
import app.fedilab.android.databinding.ActivityInstanceBinding;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.viewmodel.mastodon.InstancesVM;
import es.dmoral.toasty.Toasty;
public class InstanceActivity extends BaseActivity {
ActivityInstanceBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeDialog(this);
binding = ActivityInstanceBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (getSupportActionBar() != null)
getSupportActionBar().hide();
binding.close.setOnClickListener(view -> finish());
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
InstancesVM instancesVM = new ViewModelProvider(InstanceActivity.this).get(InstancesVM.class);
instancesVM.getInstance(BaseMainActivity.currentInstance).observe(InstanceActivity.this, instanceInfo -> {
binding.instanceContainer.setVisibility(View.VISIBLE);
binding.loader.setVisibility(View.GONE);
if (instanceInfo == null || instanceInfo.info == null) {
Toasty.error(InstanceActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show();
return;
}
Instance instance = instanceInfo.info;
binding.instanceTitle.setText(instance.title);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
binding.instanceDescription.setText(Html.fromHtml(instance.description, Html.FROM_HTML_MODE_LEGACY));
else
binding.instanceDescription.setText(Html.fromHtml(instance.description));
if (instance.description == null || instance.description.trim().length() == 0)
binding.instanceDescription.setText(getString(R.string.instance_no_description));
binding.instanceVersion.setText(instance.version);
binding.instanceUri.setText(instance.uri);
if (instance.email == null) {
binding.instanceContact.hide();
}
Glide.with(InstanceActivity.this)
.asBitmap()
.load(instance.thumbnail)
.into(binding.backGroundImage);
binding.instanceContact.setOnClickListener(v -> {
Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", instance.email, null));
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "[Mastodon] - " + instance.uri);
startActivity(Intent.createChooser(emailIntent, getString(R.string.send_email)));
});
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,114 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.style.UnderlineSpan;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
import com.bumptech.glide.Glide;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.InstanceSocial;
import app.fedilab.android.databinding.ActivityInstanceSocialBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.viewmodel.mastodon.InstanceSocialVM;
public class InstanceHealthActivity extends BaseActivity {
private ActivityInstanceSocialBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeDialog(this);
binding = ActivityInstanceSocialBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (getSupportActionBar() != null)
getSupportActionBar().hide();
binding.close.setOnClickListener(view -> finish());
SpannableString content = new SpannableString(binding.refInstance.getText().toString());
content.setSpan(new UnderlineSpan(), 0, content.length(), 0);
binding.refInstance.setText(content);
binding.refInstance.setOnClickListener(view -> {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://instances.social"));
startActivity(browserIntent);
});
checkInstance();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void checkInstance() {
InstanceSocialVM instanceSocialVM = new ViewModelProvider(InstanceHealthActivity.this).get(InstanceSocialVM.class);
instanceSocialVM.getInstances(BaseMainActivity.currentInstance.trim()).observe(InstanceHealthActivity.this, instanceSocialList -> {
if (instanceSocialList != null && instanceSocialList.instances.size() > 0) {
InstanceSocial.Instance instanceSocial = instanceSocialList.instances.get(0);
if (instanceSocial.thumbnail != null && !instanceSocial.thumbnail.equals("null"))
Glide.with(InstanceHealthActivity.this)
.asBitmap()
.load(instanceSocial.thumbnail)
.into(binding.backGroundImage);
binding.name.setText(instanceSocial.name);
if (instanceSocial.up) {
binding.up.setText(R.string.is_up);
binding.up.setTextColor(ContextCompat.getColor(InstanceHealthActivity.this, R.color.green_1));
} else {
binding.up.setText(R.string.is_down);
binding.up.setTextColor(ContextCompat.getColor(InstanceHealthActivity.this, R.color.red_1));
}
binding.uptime.setText(getString(R.string.instance_health_uptime, (instanceSocial.uptime * 100)));
if (instanceSocial.checked_at != null)
binding.checkedAt.setText(getString(R.string.instance_health_checkedat, Helper.dateToString(instanceSocial.checked_at)));
binding.values.setText(getString(R.string.instance_health_indication, instanceSocial.version, Helper.withSuffix(instanceSocial.active_users), Helper.withSuffix(instanceSocial.statuses)));
binding.instanceContainer.setVisibility(View.VISIBLE);
} else {
binding.instanceContainer.setVisibility(View.VISIBLE);
binding.mainContainer.setVisibility(View.GONE);
binding.noInstance.setVisibility(View.VISIBLE);
}
binding.loader.setVisibility(View.GONE);
});
}
}

View file

@ -0,0 +1,127 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.SpannableString;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.databinding.ActivityInstanceProfileBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.AccountAdapter;
import app.fedilab.android.viewmodel.mastodon.NodeInfoVM;
import app.fedilab.android.viewmodel.mastodon.SearchVM;
import es.dmoral.toasty.Toasty;
public class InstanceProfileActivity extends BaseActivity {
private String instance;
private ActivityInstanceProfileBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeDialog(this);
binding = ActivityInstanceProfileBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
Bundle b = getIntent().getExtras();
if (getSupportActionBar() != null)
getSupportActionBar().hide();
if (b != null)
instance = b.getString(Helper.ARG_INSTANCE, null);
if (instance == null) {
finish();
}
Button close = findViewById(R.id.close);
close.setOnClickListener(view -> finish());
NodeInfoVM nodeInfoVM = new ViewModelProvider(InstanceProfileActivity.this).get(NodeInfoVM.class);
nodeInfoVM.getNodeInfo(instance).observe(InstanceProfileActivity.this, nodeInfo -> {
if (nodeInfo == null) {
Toasty.error(InstanceProfileActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show();
finish();
return;
}
binding.name.setText(nodeInfo.metadata != null ? nodeInfo.metadata.nodeName : instance);
SpannableString descriptionSpan;
if (nodeInfo.metadata != null && nodeInfo.metadata.nodeDescription != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
descriptionSpan = new SpannableString(Html.fromHtml(nodeInfo.metadata.nodeDescription, FROM_HTML_MODE_LEGACY));
else
descriptionSpan = new SpannableString(Html.fromHtml(nodeInfo.metadata.nodeDescription));
binding.description.setText(descriptionSpan, TextView.BufferType.SPANNABLE);
}
binding.userCount.setText(Helper.withSuffix((nodeInfo.usage.users.total)));
binding.statusCount.setText(Helper.withSuffix(((nodeInfo.usage.localPosts))));
String softwareStr = nodeInfo.software.name + " - ";
binding.software.setText(softwareStr);
binding.version.setText(nodeInfo.software.version);
if (nodeInfo.metadata != null && nodeInfo.metadata.staffAccounts != null && nodeInfo.metadata.staffAccounts.size() > 0) {
SearchVM searchVM = new ViewModelProvider(InstanceProfileActivity.this).get(SearchVM.class);
List<Account> accounts = new ArrayList<>();
for (String accountURL : nodeInfo.metadata.staffAccounts) {
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, accountURL, null, "accounts", false, true, false, 0, null, null, 1)
.observe(InstanceProfileActivity.this, results -> {
if (results.accounts != null && results.accounts.size() > 0) {
accounts.add(results.accounts.get(0));
}
if (accounts.size() == nodeInfo.metadata.staffAccounts.size()) {
AccountAdapter accountsListAdapter = new AccountAdapter(accounts);
binding.lvAccounts.setAdapter(accountsListAdapter);
final LinearLayoutManager mLayoutManager;
mLayoutManager = new LinearLayoutManager(InstanceProfileActivity.this);
binding.lvAccounts.setLayoutManager(mLayoutManager);
}
});
}
}
binding.instanceContainer.setVisibility(View.VISIBLE);
binding.loader.setVisibility(View.GONE);
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,222 @@
package app.fedilab.android.activities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.BaseMainActivity.admin;
import static app.fedilab.android.BaseMainActivity.api;
import static app.fedilab.android.BaseMainActivity.client_id;
import static app.fedilab.android.BaseMainActivity.client_secret;
import static app.fedilab.android.BaseMainActivity.currentInstance;
import static app.fedilab.android.BaseMainActivity.software;
import static app.fedilab.android.helper.Helper.PREF_USER_TOKEN;
import static app.fedilab.android.helper.MastodonHelper.REDIRECT_CONTENT_WEB;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.preference.PreferenceManager;
import org.jetbrains.annotations.NotNull;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.Account;
import app.fedilab.android.client.entities.WellKnownNodeinfo;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.login.FragmentLoginMain;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.viewmodel.mastodon.OauthVM;
import es.dmoral.toasty.Toasty;
public class LoginActivity extends BaseActivity {
private final int PICK_IMPORT = 5557;
private String oldSearch;
private String autofilledInstance;
private WellKnownNodeinfo.NodeInfo nodeInfo;
private NavHostFragment host;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(LoginActivity.this);
setContentView(new FrameLayout(this));
Helper.addFragment(getSupportFragmentManager(), android.R.id.content, new FragmentLoginMain(), null, null, null);
Bundle b = getIntent().getExtras();
if (b != null) {
autofilledInstance = b.getString("instance", null);
admin = b.getBoolean("admin", false);
}
//The activity handles a redirect URI, it will extract token code and will proceed to authentication
//That happens when the user wants to use an external browser
if (getIntent() != null && getIntent().getData() != null && getIntent().getData().toString().contains(REDIRECT_CONTENT_WEB + "?code=")) {
String url = getIntent().getData().toString();
String[] val = url.split("code=");
if (val.length < 2) {
Toasty.error(LoginActivity.this, getString(R.string.toast_code_error), Toast.LENGTH_LONG).show();
return;
}
String code = val[1];
OauthVM oauthVM = new ViewModelProvider(LoginActivity.this).get(OauthVM.class);
//We are dealing with a Mastodon API
if (api == Account.API.MASTODON) {
//API call to get the user token
oauthVM.createToken(currentInstance, "authorization_code", client_id, client_secret, Helper.REDIRECT_CONTENT_WEB, Helper.OAUTH_SCOPES, code)
.observe(LoginActivity.this, tokenObj -> {
Account account = new Account();
account.client_id = BaseMainActivity.client_id;
account.client_secret = BaseMainActivity.client_secret;
account.token = tokenObj.token_type + " " + tokenObj.access_token;
account.api = api;
account.software = software;
account.instance = currentInstance;
//API call to retrieve account information for the new token
AccountsVM accountsVM = new ViewModelProvider(LoginActivity.this).get(AccountsVM.class);
accountsVM.getConnectedAccount(currentInstance, account.token).observe(LoginActivity.this, mastodonAccount -> {
account.mastodon_account = mastodonAccount;
new Thread(() -> {
try {
account.user_id = mastodonAccount.id;
//update the database
new Account(LoginActivity.this).insertOrUpdate(account);
BaseMainActivity.currentToken = account.token;
BaseMainActivity.currentUserID = account.user_id;
api = Account.API.MASTODON;
SharedPreferences.Editor editor = sharedpreferences.edit();
editor.putString(PREF_USER_TOKEN, account.token);
editor.commit();
//The user is now aut
//The user is now authenticated, it will be redirected to MainActivity
Handler mainHandler = new Handler(Looper.getMainLooper());
Runnable myRunnable = () -> {
Intent mainActivity = new Intent(LoginActivity.this, BaseMainActivity.class);
startActivity(mainActivity);
finish();
};
mainHandler.post(myRunnable);
} catch (DBException e) {
e.printStackTrace();
}
}).start();
});
});
}
}
}
@Override
protected void onResume() {
super.onResume();
}
@Override
public boolean onCreateOptionsMenu(@NotNull Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main_login, menu);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(LoginActivity.this);
boolean embedded_browser = sharedpreferences.getBoolean(getString(R.string.SET_EMBEDDED_BROWSER), true);
menu.findItem(R.id.action_custom_tabs).setChecked(!embedded_browser);
/* boolean security_provider = sharedpreferences.getBoolean(getString(R.string.SET_SECURITY_PROVIDER), true);
menu.findItem(R.id.action_provider).setChecked(security_provider);*/
return true;
}
@Override
public boolean onOptionsItemSelected(@NotNull MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_about) {
// Intent intent = new Intent(LoginActivity.this, AboutActivity.class);
// startActivity(intent);
} else if (id == R.id.action_privacy) {
// Intent intent = new Intent(LoginActivity.this, PrivacyActivity.class);
// startActivity(intent);
} else if (id == R.id.action_proxy) {
Intent intent = new Intent(LoginActivity.this, ProxyActivity.class);
startActivity(intent);
} else if (id == R.id.action_custom_tabs) {
item.setChecked(!item.isChecked());
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(LoginActivity.this);
SharedPreferences.Editor editor = sharedpreferences.edit();
editor.putBoolean(getString(R.string.SET_EMBEDDED_BROWSER), !item.isChecked());
editor.apply();
return false;
} else if (id == R.id.action_provider) {
/* item.setChecked(!item.isChecked());
SharedPreferences sharedpreferences = getSharedPreferences(Helper.APP_PREFS, MODE_PRIVATE);
SharedPreferences.Editor editor = sharedpreferences.edit();
editor.putBoolean(getString(R.string.SET_SECURITY_PROVIDER), item.isChecked());
editor.apply();*/
return false;
} else if (id == R.id.action_import_data) {
/* if (ContextCompat.checkSelfPermission(LoginActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) !=
PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(LoginActivity.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
TootActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
return true;
}*/
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
String[] mimetypes = {"*/*"};
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes);
startActivityForResult(intent, PICK_IMPORT);
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PICK_IMPORT && resultCode == RESULT_OK) {
if (data == null || data.getData() == null) {
Toasty.error(LoginActivity.this, getString(R.string.toot_select_file_error), Toast.LENGTH_LONG).show();
return;
}
// String filename = Helper.getFilePathFromURI(LoginActivity.this, data.getData());
// Sqlite.importDB(LoginActivity.this, filename);
} else {
Toasty.error(LoginActivity.this, getString(R.string.toot_select_file_error), Toast.LENGTH_LONG).show();
}
}
}

View file

@ -0,0 +1,323 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.Timeline;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.client.mastodon.entities.MastodonList;
import app.fedilab.android.databinding.ActivityListBinding;
import app.fedilab.android.databinding.PopupAddListBinding;
import app.fedilab.android.databinding.PopupManageAccountsListBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.AccountListAdapter;
import app.fedilab.android.ui.drawer.MastodonListAdapter;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.viewmodel.mastodon.TimelinesVM;
import es.dmoral.toasty.Toasty;
public class MastodonListActivity extends BaseActivity implements MastodonListAdapter.ActionOnList {
AccountListAdapter accountsInListAdapter;
private ActivityListBinding binding;
private boolean canGoBack;
private TimelinesVM timelinesVM;
private MastodonList mastodonList;
private ArrayList<MastodonList> mastodonListList;
private MastodonListAdapter mastodonListAdapter;
private AccountsVM accountsVM;
private List<Account> accountsInList;
private boolean flagLoading;
private String max_id;
private FragmentMastodonTimeline fragmentMastodonTimeline;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivityListBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
canGoBack = false;
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
flagLoading = false;
max_id = null;
accountsVM = new ViewModelProvider(MastodonListActivity.this).get(AccountsVM.class);
timelinesVM = new ViewModelProvider(MastodonListActivity.this).get(TimelinesVM.class);
timelinesVM.getLists(BaseMainActivity.currentInstance, BaseMainActivity.currentToken)
.observe(MastodonListActivity.this, mastodonLists -> {
if (mastodonLists != null && mastodonLists.size() > 0) {
mastodonListList = new ArrayList<>(mastodonLists);
mastodonListAdapter = new MastodonListAdapter(mastodonListList);
mastodonListAdapter.actionOnList = this;
binding.notContent.setVisibility(View.GONE);
binding.recyclerView.setAdapter(mastodonListAdapter);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(MastodonListActivity.this));
} else {
binding.notContent.setVisibility(View.VISIBLE);
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
} else if (item.getItemId() == R.id.action_manage_users) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MastodonListActivity.this, Helper.dialogStyle());
PopupManageAccountsListBinding popupManageAccountsListBinding = PopupManageAccountsListBinding.inflate(getLayoutInflater());
dialogBuilder.setView(popupManageAccountsListBinding.getRoot());
popupManageAccountsListBinding.loader.setVisibility(View.VISIBLE);
popupManageAccountsListBinding.searchAccount.setOnTouchListener((v, event) -> {
final int DRAWABLE_RIGHT = 2;
if (event.getAction() == MotionEvent.ACTION_UP) {
if (popupManageAccountsListBinding.searchAccount.length() > 0 && event.getRawX() >= (popupManageAccountsListBinding.searchAccount.getRight() - popupManageAccountsListBinding.searchAccount.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) {
popupManageAccountsListBinding.searchAccount.setText("");
}
}
return false;
});
timelinesVM.getAccountsInList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mastodonList.id, null, null, 10)
.observe(MastodonListActivity.this, accounts -> {
popupManageAccountsListBinding.loader.setVisibility(View.GONE);
accountsInList = accounts;
if (accountsInList == null) {
accountsInList = new ArrayList<>();
}
if (accountsInList.size() > 0) {
max_id = accountsInList.get(accountsInList.size() - 1).id;
popupManageAccountsListBinding.noContent.setVisibility(View.GONE);
popupManageAccountsListBinding.lvAccountsCurrent.setVisibility(View.VISIBLE);
} else {
popupManageAccountsListBinding.noContent.setVisibility(View.VISIBLE);
popupManageAccountsListBinding.lvAccountsCurrent.setVisibility(View.GONE);
}
accountsInListAdapter = new AccountListAdapter(mastodonList, accountsInList, null);
popupManageAccountsListBinding.lvAccountsCurrent.setAdapter(accountsInListAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(MastodonListActivity.this);
popupManageAccountsListBinding.lvAccountsCurrent.setLayoutManager(linearLayoutManager);
popupManageAccountsListBinding.lvAccountsCurrent.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
int firstVisibleItem = linearLayoutManager.findFirstVisibleItemPosition();
if (dy > 0) {
int visibleItemCount = linearLayoutManager.getChildCount();
int totalItemCount = linearLayoutManager.getItemCount();
if (firstVisibleItem + visibleItemCount == totalItemCount) {
if (!flagLoading) {
flagLoading = true;
timelinesVM.getAccountsInList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mastodonList.id, max_id, null, 10)
.observe(MastodonListActivity.this, accounts -> {
if (accounts != null && accounts.size() > 0) {
int position = accountsInList.size();
max_id = accountsInList.get(accounts.size() - 1).id;
accountsInList.addAll(accounts);
accountsInListAdapter.notifyItemRangeChanged(position, accounts.size());
}
});
}
}
}
}
});
});
popupManageAccountsListBinding.searchAccount.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (count > 0) {
popupManageAccountsListBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_close_24, 0);
} else {
popupManageAccountsListBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_search_24, 0);
}
}
@Override
public void afterTextChanged(Editable s) {
if (s != null && s.length() > 0) {
accountsVM.searchAccounts(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, s.toString(), 20, true, true)
.observe(MastodonListActivity.this, accounts -> {
popupManageAccountsListBinding.lvAccountsSearch.setVisibility(View.VISIBLE);
popupManageAccountsListBinding.lvAccountsCurrent.setVisibility(View.GONE);
AccountListAdapter accountListAdapter = new AccountListAdapter(mastodonList, accountsInList, accounts);
popupManageAccountsListBinding.lvAccountsSearch.setAdapter(accountListAdapter);
popupManageAccountsListBinding.lvAccountsSearch.setLayoutManager(new LinearLayoutManager(MastodonListActivity.this));
});
} else {
popupManageAccountsListBinding.lvAccountsSearch.setVisibility(View.GONE);
popupManageAccountsListBinding.lvAccountsCurrent.setVisibility(View.VISIBLE);
}
}
});
dialogBuilder.setPositiveButton(R.string.close, (dialog, id) -> dialog.dismiss());
dialogBuilder.create().show();
} else if (item.getItemId() == R.id.action_delete && mastodonList != null) {
AlertDialog.Builder alt_bld = new AlertDialog.Builder(MastodonListActivity.this, Helper.dialogStyle());
alt_bld.setTitle(R.string.action_lists_delete);
alt_bld.setMessage(R.string.action_lists_confirm_delete);
alt_bld.setPositiveButton(R.string.delete, (dialog, id) -> {
timelinesVM.deleteList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, mastodonList.id);
int position = 0;
for (MastodonList mastodonListTmp : mastodonListList) {
if (mastodonListTmp.id.equalsIgnoreCase(mastodonList.id)) {
break;
}
position++;
}
mastodonListList.remove(position);
mastodonListAdapter.notifyItemRemoved(position);
ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.recyclerView, () -> {
if (fragmentMastodonTimeline != null) {
fragmentMastodonTimeline.onDestroyView();
}
});
if (mastodonListList.size() == 0) {
binding.notContent.setVisibility(View.VISIBLE);
} else {
binding.notContent.setVisibility(View.GONE);
}
Bundle b = new Bundle();
b.putBoolean(Helper.RECEIVE_REDRAW_TOPBAR, true);
Intent intentBD = new Intent(Helper.BROADCAST_DATA);
b.putSerializable(Helper.RECEIVE_MASTODON_LIST, mastodonListList);
intentBD.putExtras(b);
LocalBroadcastManager.getInstance(MastodonListActivity.this).sendBroadcast(intentBD);
});
alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss());
AlertDialog alert = alt_bld.create();
alert.show();
} else if (item.getItemId() == R.id.action_add_list) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MastodonListActivity.this, Helper.dialogStyle());
PopupAddListBinding popupAddListBinding = PopupAddListBinding.inflate(getLayoutInflater());
dialogBuilder.setView(popupAddListBinding.getRoot());
popupAddListBinding.addList.setFilters(new InputFilter[]{new InputFilter.LengthFilter(255)});
dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> {
if (popupAddListBinding.addList.getText() != null && popupAddListBinding.addList.getText().toString().trim().length() > 0) {
timelinesVM.createList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, popupAddListBinding.addList.getText().toString().trim(), null)
.observe(MastodonListActivity.this, newMastodonList -> {
if (newMastodonList != null) {
mastodonListList.add(0, newMastodonList);
mastodonListAdapter.notifyItemInserted(0);
} else {
Toasty.error(MastodonListActivity.this, getString(R.string.toast_error), Toasty.LENGTH_LONG).show();
}
Bundle b = new Bundle();
b.putBoolean(Helper.RECEIVE_REDRAW_TOPBAR, true);
Intent intentBD = new Intent(Helper.BROADCAST_DATA);
b.putSerializable(Helper.RECEIVE_MASTODON_LIST, mastodonListList);
intentBD.putExtras(b);
LocalBroadcastManager.getInstance(MastodonListActivity.this).sendBroadcast(intentBD);
});
dialog.dismiss();
} else {
popupAddListBinding.addList.setError(getString(R.string.not_valid_list_name));
}
});
dialogBuilder.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss());
dialogBuilder.create().show();
}
return super.onOptionsItemSelected(item);
}
@Override
public void click(MastodonList mastodonList) {
this.mastodonList = mastodonList;
canGoBack = true;
ThemeHelper.slideViewsToLeft(binding.recyclerView, binding.fragmentContainer, () -> {
fragmentMastodonTimeline = new FragmentMastodonTimeline();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_LIST_ID, mastodonList.id);
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.LIST);
setTitle(mastodonList.title);
fragmentMastodonTimeline.setArguments(bundle);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, fragmentMastodonTimeline);
fragmentTransaction.commit();
});
invalidateOptionsMenu();
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
if (binding != null) {
if (binding.recyclerView.getVisibility() == View.VISIBLE) {
getMenuInflater().inflate(R.menu.menu_main_list, menu);
} else {
getMenuInflater().inflate(R.menu.menu_list, menu);
}
}
return true;
}
@Override
public void onBackPressed() {
if (canGoBack) {
canGoBack = false;
ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.recyclerView, () -> {
if (fragmentMastodonTimeline != null) {
fragmentMastodonTimeline.onDestroyView();
}
});
setTitle(R.string.action_lists);
invalidateOptionsMenu();
} else {
super.onBackPressed();
}
}
}

View file

@ -0,0 +1,437 @@
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
package app.fedilab.android.activities;
import android.Manifest;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Point;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.Display;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.preference.PreferenceManager;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import app.fedilab.android.R;
import app.fedilab.android.client.mastodon.entities.Attachment;
import app.fedilab.android.databinding.ActivityMediaPagerBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MediaHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.interfaces.OnDownloadInterface;
import app.fedilab.android.ui.fragment.media.FragmentMedia;
import es.dmoral.toasty.Toasty;
public class MediaActivity extends BaseActivity implements OnDownloadInterface {
int flags;
private ArrayList<Attachment> attachments;
private int mediaPosition;
private long downloadID;
private final BroadcastReceiver onDownloadComplete = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (downloadID == id) {
DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
assert manager != null;
Uri uri = manager.getUriForDownloadedFile(downloadID);
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
ContentResolver cR = context.getContentResolver();
if (cR != null && uri != null) {
shareIntent.setType(cR.getType(uri));
try {
startActivity(shareIntent);
} catch (Exception ignored) {
}
} else {
Toasty.error(context, context.getString(R.string.toast_error), Toasty.LENGTH_LONG).show();
}
} else {
Toasty.success(context, context.getString(R.string.save_over), Toasty.LENGTH_LONG).show();
}
}
};
private boolean fullscreen;
private Handler handler;
private int minTouch, maxTouch;
private float startX;
private float startY;
private FragmentMedia mCurrentFragment;
private ActivityMediaPagerBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
getWindow().requestFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
ThemeHelper.applyThemeBar(this);
super.onCreate(savedInstanceState);
ActivityCompat.postponeEnterTransition(MediaActivity.this);
binding = ActivityMediaPagerBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
fullscreen = false;
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(MediaActivity.this);
final int med_desc_timeout = sharedpreferences.getInt(getString(R.string.SET_MED_DESC_TIMEOUT), 3) * 1000;
flags = getWindow().getDecorView().getSystemUiVisibility();
Bundle b = getIntent().getExtras();
if (b != null) {
mediaPosition = b.getInt(Helper.ARG_MEDIA_POSITION, 1);
attachments = (ArrayList<Attachment>) b.getSerializable(Helper.ARG_MEDIA_ARRAY);
}
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
if (attachments == null || attachments.size() == 0)
finish();
setTitle("");
PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
binding.mediaViewpager.setAdapter(mPagerAdapter);
binding.mediaViewpager.setCurrentItem(mediaPosition - 1);
binding.mediaViewpager.setOffscreenPageLimit(0);
binding.haulerView.setOnDragDismissedListener(dragDirection -> ActivityCompat.finishAfterTransition(MediaActivity.this));
registerReceiver(onDownloadComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
String description = attachments.get(mediaPosition - 1).description;
handler = new Handler();
if (description != null && description.trim().length() > 0 && description.trim().compareTo("null") != 0) {
binding.mediaDescription.setText(description);
binding.mediaDescription.setVisibility(View.VISIBLE);
handler.postDelayed(() -> {
if (binding != null && !binding.mediaDescription.hasSelection()) {
binding.mediaDescription.setVisibility(View.GONE);
}
}, med_desc_timeout);
} else {
if (!binding.mediaDescription.hasSelection()) {
binding.mediaDescription.setVisibility(View.GONE);
}
}
binding.mediaViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
public void onPageScrollStateChanged(int state) {
}
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
public void onPageSelected(int position) {
String description = attachments.get(position).description;
if (handler != null) {
handler.removeCallbacksAndMessages(null);
}
handler = new Handler();
if (description != null && description.trim().length() > 0 && description.trim().compareTo("null") != 0) {
binding.mediaDescription.setText(description);
binding.mediaDescription.setVisibility(View.VISIBLE);
handler.postDelayed(() -> {
if (binding != null && !binding.mediaDescription.hasSelection()) {
binding.mediaDescription.setVisibility(View.GONE);
}
}, med_desc_timeout);
} else {
if (!binding.mediaDescription.hasSelection()) {
binding.mediaDescription.setVisibility(View.GONE);
}
}
}
});
setFullscreen(true);
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int screenHeight = size.y;
minTouch = (int) (screenHeight * 0.1);
maxTouch = (int) (screenHeight * 0.9);
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
getMenuInflater().inflate(R.menu.menu_media, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
ActivityCompat.finishAfterTransition(MediaActivity.this);
return true;
} else if (item.getItemId() == R.id.action_save) {
int position = binding.mediaViewpager.getCurrentItem();
Attachment attachment = attachments.get(position);
if (Build.VERSION.SDK_INT >= 23) {
if (ContextCompat.checkSelfPermission(MediaActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(MediaActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MediaActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SAVE);
} else {
if (attachment.type.compareTo("image") == 0) {
MediaHelper.manageMove(MediaActivity.this, attachment.url, false);
} else {
MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url);
downloadID = -1;
}
}
} else {
if (attachment.type.compareToIgnoreCase("image") == 0) {
MediaHelper.manageMove(MediaActivity.this, attachment.url, false);
} else {
MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url);
downloadID = -1;
}
}
} else if (item.getItemId() == R.id.action_share) {
int position = binding.mediaViewpager.getCurrentItem();
Attachment attachment = attachments.get(position);
if (attachment.type.compareTo("image") == 0) {
MediaHelper.manageMove(MediaActivity.this, attachment.url, true);
} else if (attachment.type.equalsIgnoreCase("video") || attachment.type.equalsIgnoreCase("audio") || attachment.type.equalsIgnoreCase("gifv")) {
downloadID = MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url);
} else {
if (Build.VERSION.SDK_INT >= 23) {
if (ContextCompat.checkSelfPermission(MediaActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(MediaActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MediaActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SHARE);
} else {
downloadID = MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url);
}
} else {
downloadID = MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url);
}
}
}
return true;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == Helper.EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SAVE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
int position = binding.mediaViewpager.getCurrentItem();
Attachment attachment = attachments.get(position);
if (attachment.type.compareToIgnoreCase("image") == 0) {
MediaHelper.manageMove(MediaActivity.this, attachment.url, false);
} else {
MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url);
downloadID = -1;
}
} else { /*Todo: Toast "Storage Permission Required" */ }
} else if (requestCode == Helper.EXTERNAL_STORAGE_REQUEST_CODE_MEDIA_SHARE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
int position = binding.mediaViewpager.getCurrentItem();
Attachment attachment = attachments.get(position);
downloadID = MediaHelper.manageDownloadsNoPopup(MediaActivity.this, attachment.url);
} else { /*Todo: Toast "Storage Permission Required" */ }
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(MediaActivity.this);
final int med_desc_timeout = sharedpreferences.getInt(getString(R.string.SET_MED_DESC_TIMEOUT), 3) * 1000;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = event.getX();
startY = event.getY();
break;
case MotionEvent.ACTION_UP:
float endX = event.getX();
float endY = event.getY();
if (endY > minTouch && endY < maxTouch && isAClick(startX, endX, startY, endY)) {
setFullscreen(!fullscreen);
if (!fullscreen) {
String description = attachments.get(binding.mediaViewpager.getCurrentItem()).description;
if (handler != null) {
handler.removeCallbacksAndMessages(null);
}
handler = new Handler();
if (description != null && description.trim().length() > 0 && description.trim().compareTo("null") != 0) {
binding.mediaDescription.setText(description);
binding.mediaDescription.setVisibility(View.VISIBLE);
handler.postDelayed(() -> {
if (binding != null && !binding.mediaDescription.hasSelection()) {
binding.mediaDescription.setVisibility(View.GONE);
}
}, med_desc_timeout);
} else {
if (!binding.mediaDescription.hasSelection()) {
binding.mediaDescription.setVisibility(View.GONE);
}
}
}
}
break;
}
try {
return super.dispatchTouchEvent(event);
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
}
return false;
}
private boolean isAClick(float startX, float endX, float startY, float endY) {
float differenceX = Math.abs(startX - endX);
float differenceY = Math.abs(startY - endY);
int CLICK_ACTION_THRESHOLD = 200;
return !(differenceX > CLICK_ACTION_THRESHOLD/* =5 */ || differenceY > CLICK_ACTION_THRESHOLD);
}
@Override
public void onDestroy() {
binding = null;
unregisterReceiver(onDownloadComplete);
super.onDestroy();
}
public FragmentMedia getCurrentFragment() {
return mCurrentFragment;
}
@Override
public void onDownloaded(String saveFilePath, String downloadUrl, Error error) {
}
@Override
public void onUpdateProgress(int progress) {
}
@Override
protected void onPostResume() {
super.onPostResume();
}
public boolean getFullScreen() {
return this.fullscreen;
}
public void setFullscreen(boolean fullscreen) {
this.fullscreen = fullscreen;
if (!fullscreen) {
showSystemUI();
} else {
hideSystemUI();
}
}
private void hideSystemUI() {
// Enables regular immersive mode.
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
// Hide the nav bar and status bar
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN);
}
// Shows the system bars by removing all the flags
// except for the ones that make the content appear under the system bars.
private void showSystemUI() {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
/**
* Media Pager
*/
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
@NotNull
@Override
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
FragmentMedia mediaSliderFragment = new FragmentMedia();
bundle.putInt(Helper.ARG_MEDIA_POSITION, position);
bundle.putSerializable(Helper.ARG_MEDIA_ATTACHMENT, attachments.get(position));
mediaSliderFragment.setArguments(bundle);
return mediaSliderFragment;
}
@Override
public void setPrimaryItem(@NotNull ViewGroup container, int position, @NotNull Object object) {
if (getCurrentFragment() != object) {
mCurrentFragment = ((FragmentMedia) object);
}
super.setPrimaryItem(container, position, object);
}
@Override
public int getCount() {
return attachments.size();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,108 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import androidx.preference.PreferenceManager;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivityProxyBinding;
public class ProxyActivity extends BaseActivity {
private ActivityProxyBinding binding;
private int position;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ProxyActivity.this);
binding = ActivityProxyBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (getSupportActionBar() != null)
getSupportActionBar().hide();
//Enable proxy
boolean enable_proxy = sharedpreferences.getBoolean(getString(R.string.SET_PROXY_ENABLED), false);
binding.enableProxy.setChecked(enable_proxy);
position = 0;
String hostVal = sharedpreferences.getString(getString(R.string.SET_PROXY_HOST), "127.0.0.1");
int portVal = sharedpreferences.getInt(getString(R.string.SET_PROXY_PORT), 8118);
final String login = sharedpreferences.getString(getString(R.string.SET_PROXY_LOGIN), null);
final String pwd = sharedpreferences.getString(getString(R.string.SET_PROXY_PASSWORD), null);
if (hostVal.length() > 0) {
binding.host.setText(hostVal);
}
binding.port.setText(String.valueOf(portVal));
if (login != null && login.length() > 0) {
binding.proxyLogin.setText(login);
}
if (pwd != null && binding.proxyPassword.length() > 0) {
binding.proxyPassword.setText(pwd);
}
ArrayAdapter<CharSequence> adapterTrans = ArrayAdapter.createFromResource(ProxyActivity.this,
R.array.proxy_type_choice, android.R.layout.simple_spinner_dropdown_item);
binding.type.setAdapter(adapterTrans);
binding.type.setSelection(sharedpreferences.getInt(getString(R.string.SET_PROXY_TYPE), 0), false);
binding.type.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int p, long id) {
position = p;
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
binding.setProxySave.setOnClickListener(view -> {
String hostVal1 = binding.host.getText().toString().trim();
String portVal1 = binding.port.getText().toString().trim();
String proxy_loginVal = binding.proxyLogin.getText().toString().trim();
String proxy_passwordVal = binding.proxyPassword.getText().toString().trim();
SharedPreferences.Editor editor = sharedpreferences.edit();
editor.putBoolean(getString(R.string.SET_PROXY_ENABLED), binding.enableProxy.isChecked());
editor.putInt(getString(R.string.SET_PROXY_TYPE), position);
editor.putString(getString(R.string.SET_PROXY_HOST), hostVal1);
if (portVal1.matches("\\d+"))
editor.putInt(getString(R.string.SET_PROXY_PORT), Integer.parseInt(portVal1));
editor.putString(getString(R.string.SET_PROXY_LOGIN), proxy_loginVal);
editor.putString(getString(R.string.SET_PROXY_PASSWORD), proxy_passwordVal);
editor.apply();
finish();
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,352 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.helper.PinnedTimelineHelper.sortPositionAsc;
import android.content.Intent;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.InstanceSocial;
import app.fedilab.android.client.entities.Pinned;
import app.fedilab.android.client.entities.Timeline;
import app.fedilab.android.client.entities.app.PinnedTimeline;
import app.fedilab.android.client.entities.app.RemoteInstance;
import app.fedilab.android.databinding.ActivityReorderTabsBinding;
import app.fedilab.android.databinding.PopupSearchInstanceBinding;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.helper.itemtouchhelper.OnStartDragListener;
import app.fedilab.android.helper.itemtouchhelper.OnUndoListener;
import app.fedilab.android.helper.itemtouchhelper.SimpleItemTouchHelperCallback;
import app.fedilab.android.ui.drawer.ReorderTabAdapter;
import app.fedilab.android.viewmodel.mastodon.InstanceSocialVM;
import app.fedilab.android.viewmodel.mastodon.ReorderVM;
import es.dmoral.toasty.Toasty;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class ReorderTimelinesActivity extends BaseActivity implements OnStartDragListener, OnUndoListener {
private ItemTouchHelper touchHelper;
private ReorderTabAdapter adapter;
private boolean searchInstanceRunning;
private String oldSearch;
private Pinned pinned;
private ActivityReorderTabsBinding binding;
private boolean changes;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivityReorderTabsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
searchInstanceRunning = false;
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
changes = false;
ReorderVM reorderVM = new ViewModelProvider(ReorderTimelinesActivity.this).get(ReorderVM.class);
reorderVM.getPinned().observe(ReorderTimelinesActivity.this, _pinned -> {
this.pinned = _pinned;
if (this.pinned == null) {
this.pinned = new Pinned();
this.pinned.pinnedTimelines = new ArrayList<>();
}
sortPositionAsc(this.pinned.pinnedTimelines);
adapter = new ReorderTabAdapter(this.pinned, ReorderTimelinesActivity.this, ReorderTimelinesActivity.this);
ItemTouchHelper.Callback callback =
new SimpleItemTouchHelperCallback(adapter);
touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(binding.lvReorderTabs);
binding.lvReorderTabs.setAdapter(adapter);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(ReorderTimelinesActivity.this);
binding.lvReorderTabs.setLayoutManager(mLayoutManager);
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.action_add_timeline) {
addInstance();
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
getMenuInflater().inflate(R.menu.menu_reorder, menu);
return super.onCreateOptionsMenu(menu);
}
private void addInstance() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(ReorderTimelinesActivity.this, Helper.dialogStyle());
PopupSearchInstanceBinding popupSearchInstanceBinding = PopupSearchInstanceBinding.inflate(getLayoutInflater());
dialogBuilder.setView(popupSearchInstanceBinding.getRoot());
popupSearchInstanceBinding.setAttachmentGroup.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.twitter_accounts) {
popupSearchInstanceBinding.searchInstance.setHint(R.string.list_of_twitter_accounts);
} else {
popupSearchInstanceBinding.searchInstance.setHint(R.string.instance);
}
});
popupSearchInstanceBinding.searchInstance.setFilters(new InputFilter[]{new InputFilter.LengthFilter(60)});
dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> {
String instanceName = popupSearchInstanceBinding.searchInstance.getText().toString().trim().replace("@", "");
new Thread(() -> {
String url = null;
if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.mastodon_instance)
url = "https://" + instanceName + "/api/v1/timelines/public?local=true";
else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.peertube_instance)
url = "https://" + instanceName + "/api/v1/videos/";
else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.pixelfed_instance) {
url = "https://" + instanceName + "/api/v1/timelines/public";
} else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.misskey_instance) {
url = "https://" + instanceName + "/api/notes/local-timeline";
} else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.gnu_instance) {
url = "https://" + instanceName + "/api/statuses/public_timeline.json";
}
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.proxy(Helper.getProxy(getApplication().getApplicationContext()))
.readTimeout(10, TimeUnit.SECONDS).build();
Request request;
if (url != null) {
request = new Request.Builder()
.url(url)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
e.printStackTrace();
runOnUiThread(() -> Toasty.warning(ReorderTimelinesActivity.this, getString(R.string.toast_instance_unavailable), Toast.LENGTH_LONG).show());
}
@Override
public void onResponse(@NonNull Call call, @NonNull final Response response) {
if (!response.isSuccessful()) {
runOnUiThread(() -> {
dialog.dismiss();
RemoteInstance.InstanceType instanceType = null;
if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.mastodon_instance) {
instanceType = RemoteInstance.InstanceType.MASTODON;
} else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.peertube_instance) {
instanceType = RemoteInstance.InstanceType.PEERTUBE;
} else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.pixelfed_instance) {
instanceType = RemoteInstance.InstanceType.PIXELFED;
} else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.misskey_instance) {
instanceType = RemoteInstance.InstanceType.MISSKEY;
} else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.gnu_instance) {
instanceType = RemoteInstance.InstanceType.GNU;
} else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_accounts) {
instanceType = RemoteInstance.InstanceType.NITTER;
}
RemoteInstance remoteInstance = new RemoteInstance();
remoteInstance.type = instanceType;
remoteInstance.host = instanceName;
PinnedTimeline pinnedTimeline = new PinnedTimeline();
pinnedTimeline.remoteInstance = remoteInstance;
pinnedTimeline.displayed = true;
pinnedTimeline.type = Timeline.TimeLineEnum.REMOTE;
pinnedTimeline.position = pinned.pinnedTimelines.size();
pinned.pinnedTimelines.add(pinnedTimeline);
try {
new Pinned(ReorderTimelinesActivity.this).updatePinned(pinned);
changes = true;
adapter.notifyItemInserted(pinned.pinnedTimelines.size());
} catch (DBException e) {
e.printStackTrace();
}
});
} else {
runOnUiThread(() -> Toasty.warning(ReorderTimelinesActivity.this, getString(R.string.toast_instance_unavailable), Toast.LENGTH_LONG).show());
}
}
});
} else {
runOnUiThread(() -> Toasty.warning(ReorderTimelinesActivity.this, getString(R.string.toast_instance_unavailable), Toast.LENGTH_LONG).show());
}
}).start();
});
AlertDialog alertDialog = dialogBuilder.create();
alertDialog.setOnDismissListener(dialogInterface -> {
//Hide keyboard
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
assert imm != null;
imm.hideSoftInputFromWindow(popupSearchInstanceBinding.searchInstance.getWindowToken(), 0);
});
if (alertDialog.getWindow() != null)
alertDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
alertDialog.show();
popupSearchInstanceBinding.searchInstance.setOnItemClickListener((parent, view1, position, id) -> oldSearch = parent.getItemAtPosition(position).toString().trim());
popupSearchInstanceBinding.searchInstance.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.length() > 2 && !searchInstanceRunning) {
String query = s.toString().trim();
if (query.startsWith("http://")) {
query = query.replace("http://", "");
}
if (query.startsWith("https://")) {
query = query.replace("https://", "");
}
if (oldSearch == null || !oldSearch.equals(s.toString().trim())) {
searchInstanceRunning = true;
InstanceSocialVM instanceSocialVM = new ViewModelProvider(ReorderTimelinesActivity.this).get(InstanceSocialVM.class);
instanceSocialVM.getInstances(query).observe(ReorderTimelinesActivity.this, instanceSocialList -> {
popupSearchInstanceBinding.searchInstance.setAdapter(null);
String[] instances = new String[instanceSocialList.instances.size()];
int j = 0;
for (InstanceSocial.Instance instance : instanceSocialList.instances) {
instances[j] = instance.name;
j++;
}
ArrayAdapter<String> arrayAdapter =
new ArrayAdapter<>(ReorderTimelinesActivity.this, android.R.layout.simple_list_item_1, instances);
popupSearchInstanceBinding.searchInstance.setAdapter(arrayAdapter);
if (popupSearchInstanceBinding.searchInstance.hasFocus() && !isFinishing())
popupSearchInstanceBinding.searchInstance.showDropDown();
if (oldSearch != null && oldSearch.equals(popupSearchInstanceBinding.searchInstance.getText().toString())) {
popupSearchInstanceBinding.searchInstance.dismissDropDown();
}
oldSearch = s.toString().trim();
searchInstanceRunning = false;
});
}
}
}
});
}
@Override
protected void onPause() {
super.onPause();
if (changes) {
//Update menu
Bundle b = new Bundle();
b.putBoolean(Helper.RECEIVE_REDRAW_TOPBAR, true);
Intent intentBD = new Intent(Helper.BROADCAST_DATA);
intentBD.putExtras(b);
LocalBroadcastManager.getInstance(ReorderTimelinesActivity.this).sendBroadcast(intentBD);
}
}
@Override
public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
touchHelper.startDrag(viewHolder);
}
@Override
public void onUndo(PinnedTimeline pinnedTimeline, int position) {
binding.undoContainer.setVisibility(View.VISIBLE);
switch (pinnedTimeline.type) {
case TAG:
binding.undoMessage.setText(R.string.reorder_tag_removed);
break;
case REMOTE:
binding.undoMessage.setText(R.string.reorder_instance_removed);
break;
case LIST:
binding.undoMessage.setText(R.string.reorder_list_deleted);
break;
}
binding.undoAction.setPaintFlags(binding.undoAction.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
Runnable runnable = () -> {
binding.undoContainer.setVisibility(View.GONE);
//change position of pinned that are after the removed item
for (int i = pinnedTimeline.position + 1; i < pinned.pinnedTimelines.size(); i++) {
pinned.pinnedTimelines.get(i).position -= 1;
}
pinned.pinnedTimelines.remove(pinnedTimeline);
adapter.notifyItemRemoved(position);
try {
new Pinned(ReorderTimelinesActivity.this).updatePinned(pinned);
changes = true;
} catch (DBException e) {
e.printStackTrace();
}
};
Handler handler = new Handler();
handler.postDelayed(runnable, 4000);
binding.undoAction.setOnClickListener(v -> {
pinned.pinnedTimelines.add(position, pinnedTimeline);
adapter.notifyItemInserted(position);
binding.undoContainer.setVisibility(View.GONE);
handler.removeCallbacks(runnable);
});
}
@Override
public void onStop() {
super.onStop();
}
}

View file

@ -0,0 +1,327 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.RadioButton;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.LinearLayoutCompat;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.Timeline;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.client.mastodon.entities.RelationShip;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.databinding.ActivityReportBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.RulesAdapter;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import es.dmoral.toasty.Toasty;
public class ReportActivity extends BaseActivity {
private ActivityReportBinding binding;
private Status status;
private Account account;
private AccountsVM accountsVM;
private RelationShip relationShip;
private List<String> statusIds;
private List<String> ruleIds;
private String comment;
private boolean forward;
private FragmentMastodonTimeline fragment;
private RulesAdapter rulesAdapter;
private String category;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivityReportBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Bundle b = getIntent().getExtras();
if (b != null) {
status = (Status) b.getSerializable(Helper.ARG_STATUS);
account = (Account) b.getSerializable(Helper.ARG_ACCOUNT);
}
if (account == null && status != null) {
account = status.account;
}
//The entry view
show(binding.screenReason);
setTitle(getString(R.string.report_title, "@" + account.acct));
binding.val1.setOnClickListener(v -> {
binding.actionButton.setEnabled(true);
setChecked(binding.val1);
});
binding.val2.setOnClickListener(v -> {
binding.actionButton.setEnabled(true);
setChecked(binding.val2);
});
binding.val3.setOnClickListener(v -> {
binding.actionButton.setEnabled(true);
setChecked(binding.val3);
});
binding.val4.setOnClickListener(v -> {
binding.actionButton.setEnabled(true);
setChecked(binding.val4);
});
binding.val1Container.setOnClickListener(v -> {
binding.actionButton.setEnabled(true);
setChecked(binding.val1);
});
binding.val2Container.setOnClickListener(v -> {
binding.actionButton.setEnabled(true);
setChecked(binding.val2);
});
binding.val3Container.setOnClickListener(v -> {
binding.actionButton.setEnabled(true);
setChecked(binding.val3);
});
binding.val4Container.setOnClickListener(v -> {
binding.actionButton.setEnabled(true);
setChecked(binding.val4);
});
accountsVM = new ViewModelProvider(ReportActivity.this).get(AccountsVM.class);
binding.actionButton.setOnClickListener(v -> {
if (binding.screenReason.getVisibility() == View.VISIBLE) {
if (binding.val1.isChecked()) {
show(binding.screenIdontlike);
switchToIDontLike();
} else if (binding.val2.isChecked()) {
show(binding.screenSpam);
switchToSpam();
} else if (binding.val3.isChecked()) {
show(binding.screenViolateRules);
switchToRules();
} else if (binding.val4.isChecked()) {
show(binding.screenSomethingElse);
switchToSomethingElse();
}
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void switchToRules() {
rulesAdapter = new RulesAdapter(BaseMainActivity.instanceInfo.rules);
binding.lvVr.setAdapter(rulesAdapter);
binding.lvVr.setLayoutManager(new LinearLayoutManager(ReportActivity.this));
binding.actionButton.setText(R.string.next);
binding.actionButton.setOnClickListener(v -> {
category = "violation";
show(binding.screenSomethingElse);
switchToSomethingElse();
});
}
private void switchToIDontLike() {
List<String> ids = new ArrayList<>();
ids.add(account.id);
binding.unfollowTitle.setText(getString(R.string.report_1_unfollow_title, "@" + account.acct));
binding.muteTitle.setText(getString(R.string.report_1_mute_title, "@" + account.acct));
binding.blockTitle.setText(getString(R.string.report_1_block_title, "@" + account.acct));
accountsVM.getRelationships(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, ids)
.observe(ReportActivity.this, relationShips -> {
if (relationShips != null && relationShips.size() > 0) {
relationShip = relationShips.get(0);
if (relationShip.following) {
binding.groupUnfollow.setVisibility(View.VISIBLE);
} else {
binding.groupUnfollow.setVisibility(View.GONE);
}
if (relationShip.blocking) {
binding.groupBlock.setVisibility(View.GONE);
} else {
binding.groupBlock.setVisibility(View.VISIBLE);
}
if (relationShip.muting) {
binding.groupMute.setVisibility(View.GONE);
} else {
binding.groupMute.setVisibility(View.VISIBLE);
}
}
binding.actionUnfollow.setOnClickListener(v -> accountsVM.unfollow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id)
.observe(ReportActivity.this, rsUnfollow -> {
if (rsUnfollow != null) {
relationShip = rsUnfollow;
Toasty.info(ReportActivity.this, getString(R.string.toast_unfollow), Toasty.LENGTH_LONG).show();
binding.groupUnfollow.setVisibility(View.GONE);
}
}));
binding.actionMute.setOnClickListener(v -> accountsVM.mute(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, null, null)
.observe(ReportActivity.this, rsMute -> {
if (rsMute != null) {
relationShip = rsMute;
Toasty.info(ReportActivity.this, getString(R.string.toast_mute), Toasty.LENGTH_LONG).show();
binding.groupMute.setVisibility(View.GONE);
}
}));
binding.actionBlock.setOnClickListener(v -> accountsVM.block(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id)
.observe(ReportActivity.this, rsBlock -> {
if (rsBlock != null) {
relationShip = rsBlock;
Toasty.info(ReportActivity.this, getString(R.string.toast_block), Toasty.LENGTH_LONG).show();
binding.groupBlock.setVisibility(View.GONE);
}
}));
});
binding.actionButton.setText(R.string.done);
binding.actionButton.setOnClickListener(v -> finish());
}
private void switchToSpam() {
fragment = new FragmentMastodonTimeline();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.ACCOUNT_TIMELINE);
bundle.putSerializable(Helper.ARG_ACCOUNT, account);
//Set to display statuses with less options
bundle.putBoolean(Helper.ARG_MINIFIED, true);
if (status != null) {
status.isChecked = true;
bundle.putSerializable(Helper.ARG_STATUS_REPORT, status);
}
fragment.setArguments(bundle);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fram_spam_container, fragment);
fragmentTransaction.commit();
binding.actionButton.setText(R.string.next);
binding.actionButton.setOnClickListener(v -> {
category = "spam";
switchToMoreInfo();
});
}
private void switchToSomethingElse() {
fragment = new FragmentMastodonTimeline();
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.ACCOUNT_TIMELINE);
bundle.putSerializable(Helper.ARG_ACCOUNT, account);
//Set to display statuses with less options
bundle.putBoolean(Helper.ARG_MINIFIED, true);
if (status != null) {
status.isChecked = true;
bundle.putSerializable(Helper.ARG_STATUS_REPORT, status);
}
fragment.setArguments(bundle);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fram_se_container, fragment);
fragmentTransaction.commit();
binding.actionButton.setText(R.string.next);
binding.actionButton.setOnClickListener(v -> {
if (category == null) {
category = "other";
}
switchToMoreInfo();
});
}
private void switchToMoreInfo() {
show(binding.screenMoreDetails);
String[] domains = account.acct.split("@");
if (domains.length > 1) {
binding.forward.setOnCheckedChangeListener((compoundButton, checked) -> forward = checked);
binding.forward.setText(getString(R.string.report_more_forward, domains[1]));
} else {
forward = false;
binding.forwardBlock.setVisibility(View.GONE);
}
binding.actionButton.setText(R.string.done);
binding.actionButton.setOnClickListener(v -> {
if (rulesAdapter != null) {
ruleIds = rulesAdapter.getChecked();
}
if (fragment != null) {
statusIds = fragment.getCheckedStatusesId();
}
comment = binding.reportMessage.getText().toString();
binding.actionButton.setEnabled(false);
accountsVM.report(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, category, statusIds, ruleIds, comment, forward)
.observe(ReportActivity.this, report -> {
Toasty.success(ReportActivity.this, R.string.report_sent, Toasty.LENGTH_LONG).show();
finish();
});
});
}
/**
* Hide all views and display the wanted one
*
* @param linearLayoutCompat LinearLayoutCompat - One of the report page
*/
private void show(LinearLayoutCompat linearLayoutCompat) {
binding.screenReason.setVisibility(View.GONE);
binding.screenIdontlike.setVisibility(View.GONE);
binding.screenSpam.setVisibility(View.GONE);
binding.screenViolateRules.setVisibility(View.GONE);
binding.screenSomethingElse.setVisibility(View.GONE);
binding.screenMoreDetails.setVisibility(View.GONE);
linearLayoutCompat.setVisibility(View.VISIBLE);
}
private void setChecked(RadioButton radioButton) {
binding.val1.setChecked(false);
binding.val2.setChecked(false);
binding.val3.setChecked(false);
binding.val4.setChecked(false);
radioButton.setChecked(true);
}
}

View file

@ -0,0 +1,88 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import com.google.android.material.tabs.TabLayout;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivityScheduledBinding;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.pageadapter.FedilabScheduledPageAdapter;
public class ScheduledActivity extends BaseActivity {
private ActivityScheduledBinding binding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
binding = ActivityScheduledBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
ActionBar actionBar = getSupportActionBar();
//Remove title
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
}
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
MastodonHelper.loadPPMastodon(binding.profilePicture, BaseMainActivity.accountWeakReference.get().mastodon_account);
binding.title.setText(R.string.scheduled);
binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.toots_server)));
binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.toots_client)));
binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.reblog)));
binding.scheduleViewpager.setAdapter(new FedilabScheduledPageAdapter(getSupportFragmentManager()));
binding.scheduleViewpager.setOffscreenPageLimit(3);
binding.scheduleViewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.scheduleTablayout));
binding.scheduleTablayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
binding.scheduleViewpager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,240 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.app.SearchManager;
import android.content.Context;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import org.jetbrains.annotations.NotNull;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivitySearchResultTabsBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTag;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
import es.dmoral.toasty.Toasty;
public class SearchResultTabActivity extends BaseActivity {
private String search;
private ActivitySearchResultTabsBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivitySearchResultTabsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Bundle b = getIntent().getExtras();
if (b != null) {
search = b.getString(Helper.ARG_SEARCH_KEYWORD, null);
}
if (search == null) {
Toasty.error(SearchResultTabActivity.this, getString(R.string.toast_error_search), Toast.LENGTH_LONG).show();
finish();
return;
}
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setTitle(search);
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.tags)));
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.accounts)));
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.toots)));
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.action_cache)));
binding.searchTabLayout.setTabTextColors(ThemeHelper.getAttColor(SearchResultTabActivity.this, R.attr.mTextColor), ContextCompat.getColor(SearchResultTabActivity.this, R.color.cyanea_accent_dark_reference));
binding.searchTabLayout.setTabIconTint(ThemeHelper.getColorStateList(SearchResultTabActivity.this));
binding.searchTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
binding.searchViewpager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
Fragment fragment;
if (binding.searchViewpager.getAdapter() != null) {
fragment = (Fragment) binding.searchViewpager.getAdapter().instantiateItem(binding.searchViewpager, tab.getPosition());
if (fragment instanceof FragmentMastodonAccount) {
FragmentMastodonAccount fragmentMastodonAccount = ((FragmentMastodonAccount) fragment);
fragmentMastodonAccount.scrollToTop();
} else if (fragment instanceof FragmentMastodonTimeline) {
FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
fragmentMastodonTimeline.scrollToTop();
} else if (fragment instanceof FragmentMastodonTag) {
FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment);
fragmentMastodonTag.scrollToTop();
}
}
}
});
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_search, menu);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setIconifiedByDefault(false);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
assert imm != null;
imm.hideSoftInputFromWindow(binding.searchTabLayout.getWindowToken(), 0);
query = query.replaceAll("^#+", "");
search = query.trim();
PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
binding.searchViewpager.setAdapter(mPagerAdapter);
searchView.clearFocus();
setTitle(search);
searchView.setIconified(true);
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
searchView.setOnCloseListener(() -> {
setTitle(search);
return false;
});
searchView.setOnSearchClickListener(v -> {
searchView.setQuery(search, false);
searchView.setIconified(false);
});
PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
binding.searchViewpager.setAdapter(mPagerAdapter);
binding.searchViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
TabLayout.Tab tab = binding.searchTabLayout.getTabAt(position);
if (tab != null)
tab.select();
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
return true;
}
@Override
public boolean onOptionsItemSelected(@NotNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.action_search) {
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Pager adapter for the 4 fragments
*/
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
@NotNull
@Override
public Fragment getItem(int position) {
Bundle bundle = new Bundle();
switch (position) {
case 0:
FragmentMastodonTag fragmentMastodonTag = new FragmentMastodonTag();
bundle.putString(Helper.ARG_SEARCH_KEYWORD, search);
fragmentMastodonTag.setArguments(bundle);
return fragmentMastodonTag;
case 1:
FragmentMastodonAccount fragmentMastodonAccount = new FragmentMastodonAccount();
bundle.putString(Helper.ARG_SEARCH_KEYWORD, search);
fragmentMastodonAccount.setArguments(bundle);
return fragmentMastodonAccount;
case 2:
FragmentMastodonTimeline fragmentMastodonTimeline = new FragmentMastodonTimeline();
bundle.putString(Helper.ARG_SEARCH_KEYWORD, search);
fragmentMastodonTimeline.setArguments(bundle);
return fragmentMastodonTimeline;
default:
fragmentMastodonTimeline = new FragmentMastodonTimeline();
bundle.putString(Helper.ARG_SEARCH_KEYWORD_CACHE, search);
fragmentMastodonTimeline.setArguments(bundle);
return fragmentMastodonTimeline;
}
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
}
@Override
public int getCount() {
return 4;
}
}
}

View file

@ -0,0 +1,203 @@
package app.fedilab.android.activities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.google.gson.annotations.SerializedName;
import java.util.Locale;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivitySettingsBinding;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.settings.FragmentAdministrationSettings;
import app.fedilab.android.ui.fragment.settings.FragmentComposeSettings;
import app.fedilab.android.ui.fragment.settings.FragmentInterfaceSettings;
import app.fedilab.android.ui.fragment.settings.FragmentLanguageSettings;
import app.fedilab.android.ui.fragment.settings.FragmentNotificationsSettings;
import app.fedilab.android.ui.fragment.settings.FragmentPrivacySettings;
import app.fedilab.android.ui.fragment.settings.FragmentThemingSettings;
import app.fedilab.android.ui.fragment.settings.FragmentTimelinesSettings;
public class SettingsActivity extends BaseActivity {
private ActivitySettingsBinding binding;
private boolean canGoBack;
private Fragment currentFragment;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
binding = ActivitySettingsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
canGoBack = false;
binding.setTimelines.setOnClickListener(v -> displaySettings(SettingsEnum.TIMELINES));
binding.setNotifications.setOnClickListener(v -> displaySettings(SettingsEnum.NOTIFICATIONS));
binding.setInterface.setOnClickListener(v -> displaySettings(SettingsEnum.INTERFACE));
binding.setCompose.setOnClickListener(v -> displaySettings(SettingsEnum.COMPOSE));
binding.setPrivacy.setOnClickListener(v -> displaySettings(SettingsEnum.PRIVACY));
binding.setTheming.setOnClickListener(v -> displaySettings(SettingsEnum.THEMING));
binding.setAdministration.setOnClickListener(v -> displaySettings(SettingsEnum.ADMINISTRATION));
binding.setLanguage.setOnClickListener(v -> displaySettings(SettingsEnum.LANGUAGE));
}
public void displaySettings(SettingsEnum settingsEnum) {
ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
String category = "";
switch (settingsEnum) {
case TIMELINES:
FragmentTimelinesSettings fragmentTimelinesSettings = new FragmentTimelinesSettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentTimelinesSettings);
currentFragment = fragmentTimelinesSettings;
category = getString(R.string.settings_category_label_timelines);
break;
case NOTIFICATIONS:
FragmentNotificationsSettings fragmentNotificationsSettings = new FragmentNotificationsSettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentNotificationsSettings);
currentFragment = fragmentNotificationsSettings;
category = getString(R.string.notifications);
break;
case INTERFACE:
FragmentInterfaceSettings fragmentInterfaceSettings = new FragmentInterfaceSettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentInterfaceSettings);
currentFragment = fragmentInterfaceSettings;
category = getString(R.string.settings_category_label_interface);
break;
case COMPOSE:
FragmentComposeSettings fragmentComposeSettings = new FragmentComposeSettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentComposeSettings);
currentFragment = fragmentComposeSettings;
category = getString(R.string.compose);
break;
case PRIVACY:
FragmentPrivacySettings fragmentPrivacySettings = new FragmentPrivacySettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentPrivacySettings);
currentFragment = fragmentPrivacySettings;
category = getString(R.string.action_privacy);
break;
case THEMING:
FragmentThemingSettings fragmentThemingSettings = new FragmentThemingSettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentThemingSettings);
currentFragment = fragmentThemingSettings;
category = getString(R.string.theming);
break;
case ADMINISTRATION:
FragmentAdministrationSettings fragmentAdministrationSettings = new FragmentAdministrationSettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentAdministrationSettings);
currentFragment = fragmentAdministrationSettings;
category = getString(R.string.administration);
break;
case LANGUAGE:
FragmentLanguageSettings fragmentLanguageSettings = new FragmentLanguageSettings();
fragmentTransaction.replace(R.id.fragment_container, fragmentLanguageSettings);
currentFragment = fragmentLanguageSettings;
category = getString(R.string.languages);
break;
}
String title = String.format(Locale.getDefault(), "%s - %s", getString(R.string.settings), category);
setTitle(title);
canGoBack = true;
fragmentTransaction.commit();
});
}
@Override
public void onBackPressed() {
if (canGoBack) {
canGoBack = false;
ThemeHelper.slideViewsToRight(binding.fragmentContainer, binding.buttonContainer, () -> {
if (currentFragment != null) {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
fragmentManager.beginTransaction();
fragmentTransaction.remove(currentFragment).commit();
}
});
setTitle(R.string.settings);
} else {
super.onBackPressed();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (currentFragment != null) {
currentFragment.onDestroy();
}
binding = null;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
public enum SettingsEnum {
@SerializedName("TIMELINES")
TIMELINES("TIMELINES"),
@SerializedName("NOTIFICATIONS")
NOTIFICATIONS("NOTIFICATIONS"),
@SerializedName("INTERFACE")
INTERFACE("INTERFACE"),
@SerializedName("COMPOSE")
COMPOSE("COMPOSE"),
@SerializedName("PRIVACY")
PRIVACY("PRIVACY"),
@SerializedName("THEMING")
THEMING("THEMING"),
@SerializedName("ADMINISTRATION")
ADMINISTRATION("ADMINISTRATION"),
@SerializedName("LANGUAGE")
LANGUAGE("LANGUAGE");
private final String value;
SettingsEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}

View file

@ -0,0 +1,140 @@
package app.fedilab.android.activities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.client.mastodon.entities.Accounts;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.databinding.ActivityStatusInfoBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.AccountAdapter;
import app.fedilab.android.viewmodel.mastodon.StatusesVM;
public class StatusInfoActivity extends BaseActivity {
private ActivityStatusInfoBinding binding;
private List<Account> accountList;
private AccountAdapter accountAdapter;
private String max_id;
private typeOfInfo type;
private boolean flagLoading;
private Status status;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
binding = ActivityStatusInfoBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
accountList = new ArrayList<>();
Bundle b = getIntent().getExtras();
if (b != null) {
type = (typeOfInfo) b.getSerializable(Helper.ARG_TYPE_OF_INFO);
status = (Status) b.getSerializable(Helper.ARG_STATUS);
}
if (type == null || status == null) {
finish();
return;
}
flagLoading = false;
max_id = null;
binding.title.setText(type == typeOfInfo.BOOSTED_BY ? R.string.boosted_by : R.string.favourited_by);
StatusesVM statusesVM = new ViewModelProvider(StatusInfoActivity.this).get(StatusesVM.class);
accountAdapter = new AccountAdapter(accountList);
LinearLayoutManager mLayoutManager = new LinearLayoutManager(StatusInfoActivity.this);
binding.lvAccounts.setLayoutManager(mLayoutManager);
binding.lvAccounts.setAdapter(accountAdapter);
binding.lvAccounts.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition();
if (dy > 0) {
int visibleItemCount = mLayoutManager.getChildCount();
int totalItemCount = mLayoutManager.getItemCount();
if (firstVisibleItem + visibleItemCount == totalItemCount) {
if (!flagLoading) {
flagLoading = true;
binding.loadingNextAccounts.setVisibility(View.VISIBLE);
if (type == typeOfInfo.BOOSTED_BY) {
statusesVM.rebloggedBy(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, max_id, null, null).observe(StatusInfoActivity.this, accounts -> manageView(accounts));
} else if (type == typeOfInfo.LIKED_BY) {
statusesVM.favouritedBy(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, max_id, null, null).observe(StatusInfoActivity.this, accounts -> manageView(accounts));
}
}
} else {
binding.loadingNextAccounts.setVisibility(View.GONE);
}
}
}
});
if (type == typeOfInfo.BOOSTED_BY) {
statusesVM.rebloggedBy(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, null, null, null).observe(StatusInfoActivity.this, this::manageView);
} else if (type == typeOfInfo.LIKED_BY) {
statusesVM.favouritedBy(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, status.id, null, null, null).observe(StatusInfoActivity.this, this::manageView);
}
}
private void manageView(Accounts accounts) {
flagLoading = false;
binding.loadingNextAccounts.setVisibility(View.GONE);
if (accountList != null && accounts != null && accounts.accounts != null) {
int startId = 0;
//There are some statuses present in the timeline
if (accountList.size() > 0) {
startId = accountList.size();
}
accountList.addAll(accounts.accounts);
max_id = accounts.pagination.min_id;
accountAdapter.notifyItemRangeInserted(startId, accounts.accounts.size());
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return true;
}
public enum typeOfInfo {
LIKED_BY,
BOOSTED_BY
}
}

View file

@ -0,0 +1,257 @@
package app.fedilab.android.activities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.R;
import app.fedilab.android.databinding.ActivityWebviewBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.sqlite.Sqlite;
import app.fedilab.android.webview.CustomWebview;
import app.fedilab.android.webview.FedilabWebChromeClient;
import app.fedilab.android.webview.FedilabWebViewClient;
import es.dmoral.toasty.Toasty;
public class WebviewActivity extends BaseActivity {
public static List<String> trackingDomains;
private String url;
private String peertubeLinkToFetch;
private boolean peertubeLink;
private CustomWebview webView;
private Menu defaultMenu;
private FedilabWebViewClient FedilabWebViewClient;
private ActivityWebviewBinding binding;
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
binding = ActivityWebviewBinding.inflate(getLayoutInflater());
View view = binding.getRoot();
setContentView(view);
Bundle b = getIntent().getExtras();
if (b != null) {
url = b.getString("url", null);
peertubeLinkToFetch = b.getString("peertubeLinkToFetch", null);
peertubeLink = b.getBoolean("peertubeLink", false);
}
if (url == null)
finish();
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
webView = Helper.initializeWebview(WebviewActivity.this, R.id.webview, null);
setTitle("");
webView.getSettings().setJavaScriptEnabled(true);
FedilabWebChromeClient FedilabWebChromeClient = new FedilabWebChromeClient(WebviewActivity.this, webView, binding.webviewContainer, binding.videoLayout);
FedilabWebChromeClient.setOnToggledFullscreen(fullscreen -> {
if (fullscreen) {
binding.videoLayout.setVisibility(View.VISIBLE);
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
attrs.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
getWindow().setAttributes(attrs);
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
} else {
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.flags &= ~WindowManager.LayoutParams.FLAG_FULLSCREEN;
attrs.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
getWindow().setAttributes(attrs);
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
binding.videoLayout.setVisibility(View.GONE);
}
});
webView.setWebChromeClient(FedilabWebChromeClient);
FedilabWebViewClient = new FedilabWebViewClient(WebviewActivity.this);
webView.setWebViewClient(FedilabWebViewClient);
webView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> {
if (Build.VERSION.SDK_INT >= 23) {
if (ContextCompat.checkSelfPermission(WebviewActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(WebviewActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(WebviewActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, Helper.EXTERNAL_STORAGE_REQUEST_CODE);
} else {
Helper.manageDownloads(WebviewActivity.this, url);
}
} else {
Helper.manageDownloads(WebviewActivity.this, url);
}
});
if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://"))
url = "http://" + url;
if (trackingDomains == null) {
AsyncTask.execute(() -> {
SQLiteDatabase db = Sqlite.getInstance(getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
// trackingDomains = new DomainBlockDAO(WebviewActivity.this, db).getAll();
if (trackingDomains == null)
trackingDomains = new ArrayList<>();
// Get a handler that can be used to post to the main thread
Handler mainHandler = new Handler(getMainLooper());
Runnable myRunnable = () -> webView.loadUrl(url);
mainHandler.post(myRunnable);
});
} else
webView.loadUrl(url);
}
/* public void setCount(Context context, String count) {
if (defaultMenu != null && !peertubeLink) {
MenuItem menuItem = defaultMenu.findItem(R.id.action_block);
LayerDrawable icon = (LayerDrawable) menuItem.getIcon();
CountDrawable badge;
// Reuse drawable if possible
Drawable reuse = icon.findDrawableByLayerId(R.id.ic_block_count);
if (reuse instanceof CountDrawable) {
badge = (CountDrawable) reuse;
} else {
badge = new CountDrawable(context);
}
badge.setCount(count);
icon.mutate();
icon.setDrawableByLayerId(R.id.ic_block_count, badge);
}
}*/
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
/* if (!peertubeLink)
setCount(WebviewActivity.this, "0");*/
defaultMenu = menu;
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onCreateOptionsMenu(@NotNull Menu menu) {
getMenuInflater().inflate(R.menu.main_webview, menu);
defaultMenu = menu;
if (peertubeLink) {
menu.findItem(R.id.action_go).setVisible(false);
menu.findItem(R.id.action_block).setVisible(false);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
} else if (itemId == R.id.action_block) {
List<String> domains = FedilabWebViewClient.getDomains();
final ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(WebviewActivity.this, R.layout.domains_blocked);
arrayAdapter.addAll(domains);
AlertDialog.Builder builder = new AlertDialog.Builder(WebviewActivity.this, Helper.dialogStyle());
builder.setTitle(R.string.list_of_blocked_domains);
builder.setNegativeButton(R.string.close, (dialog, which) -> dialog.dismiss());
builder.setAdapter(arrayAdapter, (dialog, which) -> {
String strName = arrayAdapter.getItem(which);
assert strName != null;
Toasty.info(WebviewActivity.this, strName, Toast.LENGTH_LONG).show();
});
builder.show();
return true;
} else if (itemId == R.id.action_go) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
startActivity(browserIntent);
} catch (Exception e) {
Toasty.error(WebviewActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show();
}
return true;
}
return super.onOptionsItemSelected(item);
}
public void setUrl(String newUrl) {
this.url = newUrl;
}
@Override
public void onPause() {
super.onPause();
if (webView != null)
webView.onPause();
}
@Override
public void onResume() {
super.onResume();
if (webView != null)
webView.onResume();
}
@Override
public void onBackPressed() {
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (webView != null)
webView.destroy();
}
}

View file

@ -0,0 +1,264 @@
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
package app.fedilab.android.activities;
import static app.fedilab.android.BaseMainActivity.api;
import static app.fedilab.android.BaseMainActivity.currentInstance;
import static app.fedilab.android.BaseMainActivity.software;
import static app.fedilab.android.helper.Helper.PREF_USER_TOKEN;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.preference.PreferenceManager;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.Account;
import app.fedilab.android.databinding.ActivityWebviewConnectBinding;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
import app.fedilab.android.viewmodel.mastodon.OauthVM;
import es.dmoral.toasty.Toasty;
public class WebviewConnectActivity extends BaseActivity {
private ActivityWebviewConnectBinding binding;
private AlertDialog alert;
private String login_url;
@SuppressWarnings("deprecation")
public static void clearCookies(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
CookieManager.getInstance().removeAllCookies(null);
CookieManager.getInstance().flush();
} else {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(context);
cookieSyncMngr.startSync();
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
cookieManager.removeSessionCookie();
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
}
}
@SuppressLint("SetJavaScriptEnabled")
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyTheme(this);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(WebviewConnectActivity.this);
binding = ActivityWebviewConnectBinding.inflate(getLayoutInflater());
View rootView = binding.getRoot();
setContentView(rootView);
Bundle b = getIntent().getExtras();
if (b != null) {
login_url = b.getString("login_url");
}
if (login_url == null)
finish();
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
assert inflater != null;
View view = inflater.inflate(R.layout.simple_bar, new LinearLayout(WebviewConnectActivity.this), false);
view.setBackground(new ColorDrawable(ContextCompat.getColor(WebviewConnectActivity.this, R.color.cyanea_primary)));
actionBar.setCustomView(view, new ActionBar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
ImageView toolbar_close = actionBar.getCustomView().findViewById(R.id.toolbar_close);
TextView toolbar_title = actionBar.getCustomView().findViewById(R.id.toolbar_title);
toolbar_close.setOnClickListener(v -> finish());
toolbar_title.setText(R.string.add_account);
}
clearCookies(WebviewConnectActivity.this);
binding.webviewConnect.getSettings().setJavaScriptEnabled(true);
String user_agent = sharedpreferences.getString(getString(R.string.SET_CUSTOM_USER_AGENT), Helper.USER_AGENT);
binding.webviewConnect.getSettings().setUserAgentString(user_agent);
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webviewConnect, true);
final ProgressBar pbar = findViewById(R.id.progress_bar);
binding.webviewConnect.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int progress) {
if (progress < 100 && pbar.getVisibility() == ProgressBar.GONE) {
pbar.setVisibility(ProgressBar.VISIBLE);
}
pbar.setProgress(progress);
if (progress == 100) {
pbar.setVisibility(ProgressBar.GONE);
}
}
});
binding.webviewConnect.setWebViewClient(new WebViewClient() {
/* @Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String x_xsrf_token = null;
String x_csrf_token = null;
if (request.getUrl().toString().contains("accounts/verify_credentials")) {
String cookies = CookieManager.getInstance().getCookie(request.getUrl().toString());
Map<String, String> requestHeaders = request.getRequestHeaders();
Iterator<Map.Entry<String, String>> it = requestHeaders.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> pair = it.next();
if (pair.getKey().compareTo("X-XSRF-TOKEN") == 0) {
x_xsrf_token = pair.getValue();
}
if (pair.getKey().compareTo("X-CSRF-TOKEN") == 0) {
x_csrf_token = pair.getValue();
}
it.remove();
}
if (x_xsrf_token != null && x_csrf_token != null) {
String finalX_xsrf_token = x_xsrf_token;
String finalX_csrf_token = x_csrf_token;
new Handler(Looper.getMainLooper()).post(() -> {
view.stopLoading();
SharedPreferences sharedpreferences1 = getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedpreferences1.edit();
String token = "X-XSRF-TOKEN= " + finalX_xsrf_token + ";X-CSRF-TOKEN= " + finalX_csrf_token + "|" + cookies;
editor.putString(Helper.PREF_KEY_OAUTH_TOKEN, token);
editor.commit();
view.setVisibility(View.GONE);
//Update the account with the token;
new UpdateAccountInfoAsyncTask(WebviewConnectActivity.this, token, clientId, clientSecret, null, instance, social);
});
}
}
return super.shouldInterceptRequest(view, request);
}*/
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
if (url.contains(Helper.REDIRECT_CONTENT_WEB)) {
String[] val = url.split("code=");
if (val.length < 2) {
Toasty.error(WebviewConnectActivity.this, getString(R.string.toast_code_error), Toast.LENGTH_LONG).show();
Intent myIntent = new Intent(WebviewConnectActivity.this, LoginActivity.class);
startActivity(myIntent);
finish();
return false;
}
String code = val[1];
OauthVM oauthVM = new ViewModelProvider(WebviewConnectActivity.this).get(OauthVM.class);
//API call to get the user token
oauthVM.createToken(currentInstance, "authorization_code", BaseMainActivity.client_id, BaseMainActivity.client_secret, Helper.REDIRECT_CONTENT_WEB, Helper.OAUTH_SCOPES, code)
.observe(WebviewConnectActivity.this, tokenObj -> {
Account account = new Account();
account.client_id = BaseMainActivity.client_id;
account.client_secret = BaseMainActivity.client_secret;
account.token = tokenObj.token_type + " " + tokenObj.access_token;
account.api = api;
account.software = software;
account.instance = currentInstance;
//API call to retrieve account information for the new token
AccountsVM accountsVM = new ViewModelProvider(WebviewConnectActivity.this).get(AccountsVM.class);
accountsVM.getConnectedAccount(currentInstance, account.token).observe(WebviewConnectActivity.this, mastodonAccount -> {
account.mastodon_account = mastodonAccount;
account.user_id = mastodonAccount.id;
new Thread(() -> {
try {
//update the database
new Account(WebviewConnectActivity.this).insertOrUpdate(account);
Handler mainHandler = new Handler(Looper.getMainLooper());
BaseMainActivity.currentToken = account.token;
BaseMainActivity.currentUserID = account.user_id;
api = Account.API.MASTODON;
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(WebviewConnectActivity.this);
SharedPreferences.Editor editor = sharedpreferences.edit();
editor.putString(PREF_USER_TOKEN, account.token);
editor.commit();
//The user is now authenticated, it will be redirected to MainActivity
Runnable myRunnable = () -> {
Intent mainActivity = new Intent(WebviewConnectActivity.this, BaseMainActivity.class);
startActivity(mainActivity);
finish();
};
mainHandler.post(myRunnable);
} catch (DBException e) {
e.printStackTrace();
}
}).start();
});
});
return true;
} else {
return false;
}
}
});
binding.webviewConnect.loadUrl(login_url);
}
@Override
public void onBackPressed() {
if (binding.webviewConnect.canGoBack()) {
binding.webviewConnect.goBack();
} else {
super.onBackPressed();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (alert != null) {
alert.dismiss();
alert = null;
}
binding.webviewConnect.destroy();
}
}

View file

@ -0,0 +1,83 @@
package app.fedilab.android.broadcastreceiver;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import java.util.HashSet;
import java.util.Set;
/**
* Original work from https://stackoverflow.com/a/25873554
*/
public class NetworkStateReceiver extends BroadcastReceiver {
protected Set<NetworkStateReceiverListener> listeners;
protected Boolean connected;
public NetworkStateReceiver() {
listeners = new HashSet<>();
connected = null;
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getExtras() == null)
return;
ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo ni = manager.getActiveNetworkInfo();
if (ni != null && ni.getState() == NetworkInfo.State.CONNECTED) {
connected = true;
} else if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, Boolean.FALSE)) {
connected = false;
}
notifyStateToAll();
}
private void notifyStateToAll() {
for (NetworkStateReceiverListener listener : listeners)
notifyState(listener);
}
private void notifyState(NetworkStateReceiverListener listener) {
if (connected == null || listener == null)
return;
if (connected)
listener.networkAvailable();
else
listener.networkUnavailable();
}
public void addListener(NetworkStateReceiverListener l) {
listeners.add(l);
notifyState(l);
}
public void removeListener(NetworkStateReceiverListener l) {
listeners.remove(l);
}
public interface NetworkStateReceiverListener {
void networkAvailable();
void networkUnavailable();
}
}

View file

@ -0,0 +1,53 @@
package app.fedilab.android.broadcastreceiver;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.helper.Helper.RECEIVE_TOAST_CONTENT;
import static app.fedilab.android.helper.Helper.RECEIVE_TOAST_TYPE;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import app.fedilab.android.helper.Helper;
import es.dmoral.toasty.Toasty;
public class ToastMessage extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Bundle b = intent.getExtras();
if (b != null) {
String type = b.getString(RECEIVE_TOAST_TYPE, null);
String content = b.getString(RECEIVE_TOAST_CONTENT, null);
if (type != null && content != null) {
switch (type) {
case Helper.RECEIVE_TOAST_TYPE_ERROR:
Toasty.error(context, content, Toasty.LENGTH_SHORT).show();
break;
case Helper.RECEIVE_TOAST_TYPE_WARNING:
Toasty.warning(context, content, Toasty.LENGTH_SHORT).show();
break;
case Helper.RECEIVE_TOAST_TYPE_INFO:
Toasty.info(context, content, Toasty.LENGTH_SHORT).show();
break;
case Helper.RECEIVE_TOAST_TYPE_SUCCESS:
Toasty.success(context, content, Toasty.LENGTH_SHORT).show();
break;
}
}
}
}
}

View file

@ -0,0 +1,33 @@
package app.fedilab.android.client;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import app.fedilab.android.client.entities.WellKnownNodeinfo;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
public interface NodeInfoService {
@GET(".well-known/nodeinfo")
Call<WellKnownNodeinfo> getWellKnownNodeinfoLinks();
@GET("{nodeInfoPath}")
Call<WellKnownNodeinfo.NodeInfo> getNodeinfo(
@Path(value = "nodeInfoPath", encoded = true) String nodeInfoPath
);
}

View file

@ -0,0 +1,441 @@
package app.fedilab.android.client.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.sqlite.Sqlite;
/**
* Class that manages Accounts from database
* Accounts details are serialized and can be for different softwares
* The type of the software is stored in api field
*/
public class Account implements Serializable {
private final SQLiteDatabase db;
@SerializedName("user_id")
public String user_id;
@SerializedName("instance")
public String instance;
@SerializedName("api")
public API api;
@SerializedName("software")
public String software;
@SerializedName("token")
public String token;
@SerializedName("refresh_token")
public String refresh_token;
@SerializedName("token_validity")
public long token_validity;
@SerializedName("client_id")
public String client_id;
@SerializedName("client_secret")
public String client_secret;
@SerializedName("created_at")
public Date created_at;
@SerializedName("updated_at")
public Date updated_at;
@SerializedName("mastodon_account")
public app.fedilab.android.client.mastodon.entities.Account mastodon_account;
private transient Context context;
public Account() {
db = null;
}
public Account(Context context) {
//Creation of the DB with tables
this.context = context;
this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
}
/**
* Serialized a Mastodon Account class
*
* @param mastodon_account {@link app.fedilab.android.client.mastodon.entities.Account} to serialize
* @return String serialized account
*/
public static String mastodonAccountToStringStorage(app.fedilab.android.client.mastodon.entities.Account mastodon_account) {
Gson gson = new Gson();
try {
return gson.toJson(mastodon_account);
} catch (Exception e) {
return null;
}
}
/**
* Returns all Account in db
*
* @return Account List<Account>
*/
public List<Account> getPushNotificationAccounts() {
try {
Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, "(" + Sqlite.COL_API + " = 'MASTODON' OR " + Sqlite.COL_API + " = 'PLEROMA') AND " + Sqlite.COL_TOKEN + " IS NOT NULL", null, null, null, Sqlite.COL_INSTANCE + " ASC", null);
return cursorToListUserWithOwner(c);
} catch (Exception e) {
return null;
}
}
/**
* Unserialized a Mastodon Account
*
* @param serializedAccount String serialized account
* @return {@link app.fedilab.android.client.mastodon.entities.Account}
*/
public static app.fedilab.android.client.mastodon.entities.Account restoreAccountFromString(String serializedAccount) {
Gson gson = new Gson();
try {
return gson.fromJson(serializedAccount, app.fedilab.android.client.mastodon.entities.Account.class);
} catch (Exception e) {
return null;
}
}
/**
* Insert or update a user
*
* @param account {@link Account}
* @return long - db id
* @throws DBException exception with database
*/
public long insertOrUpdate(Account account) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
boolean exists = accountExist(account);
long idReturned;
if (exists) {
idReturned = updateAccount(account);
} else {
idReturned = insertAccount(account);
}
return idReturned;
}
/**
* Insert an account in db
*
* @param account {@link Account}
* @return long - db id
* @throws DBException exception with database
*/
private long insertAccount(Account account) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_APP_CLIENT_ID, account.client_id);
values.put(Sqlite.COL_APP_CLIENT_SECRET, account.client_secret);
values.put(Sqlite.COL_USER_ID, account.user_id);
values.put(Sqlite.COL_INSTANCE, account.instance);
values.put(Sqlite.COL_API, account.api.name());
values.put(Sqlite.COL_SOFTWARE, account.software);
values.put(Sqlite.COL_TOKEN_VALIDITY, account.token_validity);
values.put(Sqlite.COL_TOKEN, account.token);
values.put(Sqlite.COL_REFRESH_TOKEN, account.refresh_token);
if (account.mastodon_account != null) {
values.put(Sqlite.COL_ACCOUNT, mastodonAccountToStringStorage(account.mastodon_account));
}
values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date()));
values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date()));
//Inserts token
try {
return db.insertOrThrow(Sqlite.TABLE_USER_ACCOUNT, null, values);
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Update an account in db
*
* @param account {@link Account}
* @return long - db id
* @throws DBException exception with database
*/
private long updateAccount(Account account) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
//Can be null if only retrieving account details - IE : not the whole authentication process
if (account.client_id != null) {
values.put(Sqlite.COL_APP_CLIENT_ID, account.client_id);
values.put(Sqlite.COL_APP_CLIENT_SECRET, account.client_secret);
values.put(Sqlite.COL_API, account.api.name());
values.put(Sqlite.COL_SOFTWARE, account.software);
values.put(Sqlite.COL_TOKEN_VALIDITY, account.token_validity);
values.put(Sqlite.COL_TOKEN, account.token);
values.put(Sqlite.COL_REFRESH_TOKEN, account.refresh_token);
}
if (account.mastodon_account != null) {
values.put(Sqlite.COL_ACCOUNT, mastodonAccountToStringStorage(account.mastodon_account));
}
values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date()));
//Inserts token
try {
return db.update(Sqlite.TABLE_USER_ACCOUNT,
values, Sqlite.COL_USER_ID + " = ? AND " + Sqlite.COL_INSTANCE + " =?",
new String[]{account.user_id, account.instance});
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Check if a user exists in db
*
* @param account Account {@link Account}
* @return boolean - user exists
* @throws DBException Exception
*/
public boolean accountExist(Account account) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_USER_ACCOUNT
+ " where " + Sqlite.COL_USER_ID + " = '" + account.user_id + "' AND " + Sqlite.COL_INSTANCE + " = '" + account.instance + "'", null);
mCount.moveToFirst();
int count = mCount.getInt(0);
mCount.close();
return (count > 0);
}
/**
* Returns an Account by token
*
* @param userId String
* @param instance String
* @return Account {@link Account}
*/
public Account getUniqAccount(String userId, String instance) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, Sqlite.COL_USER_ID + " = \"" + userId + "\" AND " + Sqlite.COL_INSTANCE + " = \"" + instance + "\"", null, null, null, null, "1");
return cursorToUser(c);
} catch (Exception e) {
return null;
}
}
/**
* Returns authenticated Account
*
* @return Account {@link Account}
*/
public Account getConnectedAccount() throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, Sqlite.COL_TOKEN + " = '" + BaseMainActivity.currentToken + "'", null, null, null, null, "1");
return cursorToUser(c);
} catch (Exception e) {
return null;
}
}
/**
* Returns all accounts that allows cross-account actions
*
* @return Account List<{@link Account}>
*/
public List<Account> getCrossAccounts() throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, Sqlite.COL_API + " = 'MASTODON'", null, null, null, null, null);
return cursorToListUser(c);
} catch (Exception e) {
return null;
}
}
/**
* Returns all accounts
*
* @return Account List<{@link Account}>
*/
public List<Account> getAll() throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, null, null, null, null, null, null);
return cursorToListUser(c);
} catch (Exception e) {
return null;
}
}
/**
* Returns last used account
*
* @return Account {@link Account}
*/
public Account getLastUsedAccount() throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_USER_ACCOUNT, null, null, null, null, null, Sqlite.COL_UPDATED_AT + " DESC", "1");
return cursorToUser(c);
} catch (Exception e) {
return null;
}
}
/**
* Remove an account from db
*
* @param account {@link Account}
* @return int
*/
public int removeUser(Account account) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
return db.delete(Sqlite.TABLE_USER_ACCOUNT, Sqlite.COL_USER_ID + " = '" + account.user_id +
"' AND " + Sqlite.COL_INSTANCE + " = '" + account.instance + "'", null);
}
private List<Account> cursorToListUser(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
List<Account> accountList = new ArrayList<>();
while (c.moveToNext()) {
Account account = convertCursorToAccount(c);
//We don't add in the list the current connected account
if (!account.token.equalsIgnoreCase(BaseMainActivity.currentToken)) {
accountList.add(account);
}
}
//Close the cursor
c.close();
return accountList;
}
private List<Account> cursorToListUserWithOwner(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
List<Account> accountList = new ArrayList<>();
while (c.moveToNext()) {
Account account = convertCursorToAccount(c);
//We don't add in the list the current connected account
accountList.add(account);
}
//Close the cursor
c.close();
return accountList;
}
/***
* Method to hydrate an Account from database
* @param c Cursor
* @return Account {@link Account}
*/
private Account cursorToUser(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
//Take the first element
c.moveToFirst();
//New user
Account account = convertCursorToAccount(c);
//Close the cursor
c.close();
return account;
}
/**
* Read cursor and hydrate without closing it
*
* @param c - Cursor
* @return Account
*/
private Account convertCursorToAccount(Cursor c) {
Account account = new Account();
account.user_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID));
account.client_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_APP_CLIENT_ID));
account.client_secret = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_APP_CLIENT_SECRET));
account.token = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_TOKEN));
account.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE));
account.refresh_token = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_REFRESH_TOKEN));
account.token_validity = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_TOKEN_VALIDITY));
account.created_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_CREATED_AT)));
account.updated_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_UPDATED_AT)));
account.software = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_SOFTWARE));
String apiStr = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_API));
API api = null;
switch (apiStr) {
case "MASTODON":
api = API.MASTODON;
break;
case "PEERTUBE":
api = API.PEERTUBE;
break;
case "PIXELFED":
api = API.PIXELFED;
break;
}
account.api = api;
if (api == API.MASTODON) {
account.mastodon_account = restoreAccountFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_ACCOUNT)));
}
return account;
}
public enum API {
MASTODON,
PEERTUBE,
PIXELFED
}
}

View file

@ -0,0 +1,55 @@
package app.fedilab.android.client.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.util.Date;
import java.util.List;
public class InstanceSocial {
@SerializedName("instances")
public List<Instance> instances;
public static class Instance {
@SerializedName("name")
public String name;
@SerializedName("added_at")
public Date added_at;
@SerializedName("updated_at")
public Date updated_at;
@SerializedName("checked_at")
public Date checked_at;
@SerializedName("uptime")
public float uptime;
@SerializedName("up")
public boolean up;
@SerializedName("version")
public String version;
@SerializedName("thumbnail")
public String thumbnail;
@SerializedName("dead")
public boolean dead;
@SerializedName("active_users")
public int active_users;
@SerializedName("statuses")
public int statuses;
@SerializedName("email")
public String email;
@SerializedName("admin")
public String admin;
}
}

View file

@ -0,0 +1,212 @@
package app.fedilab.android.client.entities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.client.entities.app.PinnedTimeline;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.sqlite.Sqlite;
public class Pinned implements Serializable {
private final SQLiteDatabase db;
@SerializedName("id")
public long id = -1;
@SerializedName("instance")
public String instance;
@SerializedName("user_id")
public String user_id;
@SerializedName("pinnedTimelines")
public List<PinnedTimeline> pinnedTimelines;
@SerializedName("created_at")
public Date created_ad;
@SerializedName("updated_at")
public Date updated_at;
private Context context;
public Pinned() {
db = null;
}
public Pinned(Context context) {
//Creation of the DB with tables
this.context = context;
this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
}
/**
* Serialized a list of PinnedTimeline class
*
* @param pinnedTimelines List of {@link PinnedTimeline} to serialize
* @return String serialized pinnedTimelines list
*/
public static String mastodonPinnedTimelinesToStringStorage(List<PinnedTimeline> pinnedTimelines) {
Gson gson = new Gson();
try {
return gson.toJson(pinnedTimelines);
} catch (Exception e) {
return null;
}
}
/**
* Unserialized a PinnedTimeline List
*
* @param serializedPinnedTimelines String serialized PinnedTimeline list
* @return List of {@link PinnedTimeline}
*/
public static List<PinnedTimeline> restorePinnedTimelinesFromString(String serializedPinnedTimelines) {
Gson gson = new Gson();
try {
return gson.fromJson(serializedPinnedTimelines, new TypeToken<List<PinnedTimeline>>() {
}.getType());
} catch (Exception e) {
return null;
}
}
/**
* Insert pinnedTimeline in db
*
* @param pinned {@link Pinned}
* @return long - db id
* @throws DBException exception with database
*/
public long insertPinned(Pinned pinned) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_INSTANCE, BaseMainActivity.currentInstance);
values.put(Sqlite.COL_USER_ID, BaseMainActivity.currentUserID);
values.put(Sqlite.COL_PINNED_TIMELINES, mastodonPinnedTimelinesToStringStorage(pinned.pinnedTimelines));
values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date()));
//Inserts pinned
try {
return db.insertOrThrow(Sqlite.TABLE_PINNED_TIMELINES, null, values);
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* update pinned in db
*
* @param pinned {@link Pinned}
* @return long - db id
* @throws DBException exception with database
*/
public long updatePinned(Pinned pinned) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_PINNED_TIMELINES, mastodonPinnedTimelinesToStringStorage(pinned.pinnedTimelines));
values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date()));
//Inserts token
try {
return db.update(Sqlite.TABLE_PINNED_TIMELINES,
values, Sqlite.COL_INSTANCE + " = ? AND " + Sqlite.COL_USER_ID + " = ?",
new String[]{pinned.instance, pinned.user_id});
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Returns the pinned timeline for an account
*
* @param account Account
* @return Pinned - {@link Pinned}
*/
public Pinned getPinned(Account account) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_PINNED_TIMELINES, null, Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + account.user_id + "'", null, null, null, Sqlite.COL_UPDATED_AT + " DESC", "1");
return cursorToPined(c);
} catch (Exception e) {
return null;
}
}
public boolean pinnedExist(Pinned pinned) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_PINNED_TIMELINES
+ " where " + Sqlite.COL_INSTANCE + " = '" + pinned.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + pinned.user_id + "'", null);
mCount.moveToFirst();
int count = mCount.getInt(0);
mCount.close();
return (count > 0);
}
/**
* Restore pinned from db
*
* @param c Cursor
* @return Pinned
*/
private Pinned cursorToPined(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
//Take the first element
c.moveToFirst();
Pinned pinned = convertCursorToStatusDraft(c);
//Close the cursor
c.close();
return pinned;
}
/**
* Read cursor and hydrate without closing it
*
* @param c - Cursor
* @return Timeline
*/
private Pinned convertCursorToStatusDraft(Cursor c) {
Pinned pinned = new Pinned();
pinned.id = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_ID));
pinned.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE));
pinned.user_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID));
pinned.pinnedTimelines = restorePinnedTimelinesFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_PINNED_TIMELINES)));
pinned.created_ad = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_CREATED_AT)));
pinned.updated_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_UPDATED_AT)));
return pinned;
}
}

View file

@ -0,0 +1,40 @@
package app.fedilab.android.client.entities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.List;
public class PostState implements Serializable {
@SerializedName("number_of_posts")
public int number_of_posts;
@SerializedName("posts_successfully_sent")
public int posts_successfully_sent;
@SerializedName("posts")
public List<Post> posts;
public static class Post implements Serializable {
@SerializedName("id")
public String id;
@SerializedName("in_reply_to_id")
public String in_reply_to_id;
@SerializedName("number_of_media")
public int number_of_media;
}
}

View file

@ -0,0 +1,225 @@
package app.fedilab.android.client.entities;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static app.fedilab.android.client.entities.StatusCache.mastodonStatusToStringStorage;
import static app.fedilab.android.client.entities.StatusCache.restoreStatusFromString;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.sqlite.Sqlite;
public class ScheduledBoost implements Serializable {
private transient final SQLiteDatabase db;
@SerializedName("id")
public long id = -1;
@SerializedName("instance")
public String instance;
@SerializedName("userId")
public String userId;
@SerializedName("statusId")
public String statusId;
@SerializedName("status")
public Status status;
@SerializedName("reblogged")
public int reblogged;
@SerializedName("workerUuid")
public UUID workerUuid;
@SerializedName("scheduledAt")
public Date scheduledAt;
private Context context;
public ScheduledBoost() {
db = null;
}
public ScheduledBoost(Context context) {
//Creation of the DB with tables
this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
this.context = context;
}
/**
* Serialized a UUID
*
* @param uuid {@link UUID} to serialize
* @return String serialized uuid
*/
public static String uuidToStringStorage(UUID uuid) {
Gson gson = new Gson();
try {
return gson.toJson(uuid);
} catch (Exception e) {
return null;
}
}
/**
* Unserialized a UUID
*
* @param serializedUUID String serialized UUID
* @return {@link UUID}
*/
public static UUID restoreUuidFromString(String serializedUUID) {
Gson gson = new Gson();
try {
return gson.fromJson(serializedUUID, UUID.class);
} catch (Exception e) {
return null;
}
}
/**
* * Remove a scheduled boost
*
* @param instance - String
* @param userId - String
* @param statusId - String
* @return long
* @throws DBException exception
*/
public int removeScheduled(String instance, String userId, String statusId) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
return db.delete(Sqlite.TABLE_SCHEDULE_BOOST, Sqlite.COL_INSTANCE + " = '" + instance + "' AND "
+ Sqlite.COL_USER_ID + " = '" + userId + "' AND "
+ Sqlite.COL_STATUS_ID + " = '" + statusId + "'"
, null);
}
/**
* Insert scheduled boost in db
*
* @param scheduledBoost - ScheduledBoost
* @return long - db id
* @throws DBException exception with database
*/
public long insertScheduledBoost(ScheduledBoost scheduledBoost) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_INSTANCE, BaseMainActivity.currentInstance);
values.put(Sqlite.COL_USER_ID, BaseMainActivity.currentUserID);
values.put(Sqlite.COL_STATUS_ID, scheduledBoost.statusId);
values.put(Sqlite.COL_WORKER_UUID, uuidToStringStorage(scheduledBoost.workerUuid));
values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(scheduledBoost.status));
values.put(Sqlite.COL_SCHEDULED_AT, Helper.dateToString(scheduledBoost.scheduledAt));
values.put(Sqlite.COL_REBLOGGED, 0);
//Inserts scheduled
try {
return db.insertOrThrow(Sqlite.TABLE_SCHEDULE_BOOST, null, values);
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* * Remove a scheduled boost
*
* @param scheduledBoost - ScheduledBoost
* @return long
* @throws DBException exception
*/
public int removeScheduled(ScheduledBoost scheduledBoost) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
return db.delete(Sqlite.TABLE_SCHEDULE_BOOST, Sqlite.COL_INSTANCE + " = '" + scheduledBoost.instance + "' AND "
+ Sqlite.COL_USER_ID + " = '" + scheduledBoost.userId + "' AND "
+ Sqlite.COL_STATUS_ID + " = '" + scheduledBoost.statusId + "'"
, null);
}
/**
* Returns the ScheduledBoost for an account that has been scheduled by the client
*
* @param account Account
* @return List<ScheduledBoost> - List of {@link ScheduledBoost}
*/
public List<ScheduledBoost> getScheduled(Account account) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_SCHEDULE_BOOST, null, Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + account.user_id + "' AND " + Sqlite.COL_WORKER_UUID + " != ''", null, null, null, Sqlite.COL_ID + " DESC", null);
return cursorToScheduledBoost(c);
} catch (Exception e) {
return null;
}
}
/**
* Restore scheduledBoost list from db
*
* @param c Cursor
* @return List<ScheduledBoost>
*/
private List<ScheduledBoost> cursorToScheduledBoost(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
List<ScheduledBoost> scheduledBoosts = new ArrayList<>();
while (c.moveToNext()) {
ScheduledBoost scheduledBoost = convertCursorToScheduledBoost(c);
scheduledBoosts.add(scheduledBoost);
}
//Close the cursor
c.close();
return scheduledBoosts;
}
/**
* Read cursor and hydrate without closing it
*
* @param c - Cursor
* @return Timeline
*/
private ScheduledBoost convertCursorToScheduledBoost(Cursor c) {
ScheduledBoost scheduledBoost = new ScheduledBoost();
scheduledBoost.id = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_ID));
scheduledBoost.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE));
scheduledBoost.userId = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID));
scheduledBoost.statusId = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_STATUS_ID));
scheduledBoost.reblogged = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_REBLOGGED));
scheduledBoost.status = restoreStatusFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_STATUS)));
scheduledBoost.scheduledAt = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_SCHEDULED_AT)));
scheduledBoost.workerUuid = ScheduledBoost.restoreUuidFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_WORKER_UUID)));
return scheduledBoost;
}
}

View file

@ -0,0 +1,327 @@
package app.fedilab.android.client.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import app.fedilab.android.client.mastodon.entities.Pagination;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.client.mastodon.entities.Statuses;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.sqlite.Sqlite;
public class StatusCache {
private final SQLiteDatabase db;
@SerializedName("id")
public long id;
@SerializedName("user_id")
public String user_id;
@SerializedName("instance")
public String instance;
@SerializedName("type")
public CacheEnum type;
@SerializedName("status_id")
public String status_id;
@SerializedName("status")
public Status status;
@SerializedName("created_at")
public Date created_at;
@SerializedName("updated_at")
public Date updated_at;
private Context context;
public StatusCache() {
db = null;
}
public StatusCache(Context context) {
//Creation of the DB with tables
this.context = context;
this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
}
/**
* Serialized a Status class
*
* @param mastodon_status {@link Status} to serialize
* @return String serialized status
*/
public static String mastodonStatusToStringStorage(Status mastodon_status) {
Gson gson = new Gson();
try {
return gson.toJson(mastodon_status);
} catch (Exception e) {
return null;
}
}
/**
* Unserialized a Mastodon Status
*
* @param serializedStatus String serialized status
* @return {@link Status}
*/
public static Status restoreStatusFromString(String serializedStatus) {
Gson gson = new Gson();
try {
return gson.fromJson(serializedStatus, Status.class);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Insert or update a status
*
* @param statusCache {@link StatusCache}
* @return long - db id
* @throws DBException exception with database
*/
public long insertOrUpdate(StatusCache statusCache) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
boolean exists = statusExist(statusCache);
long idReturned;
if (exists) {
idReturned = updateStatus(statusCache);
} else {
idReturned = insertStatus(statusCache);
}
return idReturned;
}
/**
* Check if a status exists in db
*
* @param statusCache Status {@link StatusCache}
* @return boolean - StatusCache exists
* @throws DBException Exception
*/
public boolean statusExist(StatusCache statusCache) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_STATUS_CACHE
+ " where " + Sqlite.COL_STATUS_ID + " = '" + statusCache.status_id + "'"
+ " AND " + Sqlite.COL_INSTANCE + " = '" + statusCache.instance + "'"
+ " AND " + Sqlite.COL_USER_ID + "= '" + statusCache.user_id + "'", null);
mCount.moveToFirst();
int count = mCount.getInt(0);
mCount.close();
return (count > 0);
}
/**
* Insert a status in db
*
* @param statusCache {@link StatusCache}
* @return long - db id
* @throws DBException exception with database
*/
private long insertStatus(StatusCache statusCache) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_USER_ID, statusCache.user_id);
values.put(Sqlite.COL_INSTANCE, statusCache.instance);
values.put(Sqlite.COL_TYPE, statusCache.type.getValue());
values.put(Sqlite.COL_STATUS_ID, statusCache.status_id);
values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(statusCache.status));
values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date()));
//Inserts token
try {
return db.insertOrThrow(Sqlite.TABLE_STATUS_CACHE, null, values);
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Update a status in db
*
* @param statusCache {@link StatusCache}
* @return long - db id
* @throws DBException exception with database
*/
private long updateStatus(StatusCache statusCache) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_USER_ID, statusCache.user_id);
values.put(Sqlite.COL_TYPE, statusCache.type.getValue());
values.put(Sqlite.COL_STATUS_ID, statusCache.status_id);
values.put(Sqlite.COL_STATUS, mastodonStatusToStringStorage(statusCache.status));
values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date()));
//Inserts token
try {
return db.update(Sqlite.TABLE_STATUS_CACHE,
values, Sqlite.COL_STATUS_ID + " = ? AND " + Sqlite.COL_INSTANCE + " =?",
new String[]{statusCache.status_id, statusCache.instance});
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Get paginated statuses from db
*
* @param type CacheEnum - not used yet but will allow to extend cache to other timelines
* @param instance String - instance
* @param user_id String - us
* @param max_id String - status having max id
* @param min_id String - status having min id
* @return Statuses
* @throws DBException - throws a db exception
*/
public Statuses geStatuses(CacheEnum type, String instance, String user_id, String max_id, String min_id) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
String selection = Sqlite.COL_INSTANCE + "='" + instance + "' AND " + Sqlite.COL_USER_ID + "= '" + user_id + "'";
String limit = String.valueOf(MastodonHelper.statusesPerCall(context));
if (max_id == null && min_id != null) {
selection += "AND " + Sqlite.COL_STATUS_ID + " >= '" + min_id + "'";
} else if (max_id != null && min_id == null) {
selection += "AND " + Sqlite.COL_STATUS_ID + " <= '" + max_id + "'";
} else if (max_id != null) {
selection += "AND " + Sqlite.COL_STATUS_ID + " >= '" + min_id + "' AND " + Sqlite.COL_STATUS_ID + " <= '" + max_id + "'";
limit = null;
}
try {
Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, null, null, Sqlite.COL_STATUS_ID + " DESC", limit);
return createStatusReply(cursorToListOfStatuses(c));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* @param type CacheEnum - not used yet but will allow to extend cache to other timelines
* @param instance String - instance
* @param user_id String - us
* @param search String search
* @return - List<Status>
* @throws DBException exception
*/
public List<Status> searchStatus(CacheEnum type, String instance, String user_id, String search) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
String selection = Sqlite.COL_INSTANCE + "='" + instance
+ "' AND " + Sqlite.COL_USER_ID + "= '" + user_id + "'";
List<Status> reply = new ArrayList<>();
try {
Cursor c = db.query(Sqlite.TABLE_STATUS_CACHE, null, selection, null, null, null, Sqlite.COL_STATUS_ID + " DESC", "");
List<Status> statuses = cursorToListOfStatuses(c);
if (statuses != null && statuses.size() > 0) {
for (Status status : statuses) {
if (status.content.toLowerCase().contains(search.trim().toLowerCase())) {
reply.add(status);
}
}
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
return reply;
}
/**
* Convert a cursor to list of statuses
*
* @param c Cursor
* @return List<Status>
*/
private List<Status> cursorToListOfStatuses(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
List<Status> statusList = new ArrayList<>();
while (c.moveToNext()) {
Status status = convertCursorToStatus(c);
statusList.add(status);
}
//Close the cursor
c.close();
return statusList;
}
/**
* Create a reply from db in the same way than API call
*
* @param statusList List<Status>
* @return Statuses (with pagination)
*/
private Statuses createStatusReply(List<Status> statusList) {
Statuses statuses = new Statuses();
statuses.statuses = statusList;
Pagination pagination = new Pagination();
if (statusList != null && statusList.size() > 0) {
pagination.max_id = statusList.get(0).id;
pagination.min_id = statusList.get(statusList.size() - 1).id;
}
statuses.pagination = pagination;
return statuses;
}
/**
* Read cursor and hydrate without closing it
*
* @param c - Cursor
* @return Status
*/
private Status convertCursorToStatus(Cursor c) {
String serializedStatus = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_STATUS));
return restoreStatusFromString(serializedStatus);
}
public enum CacheEnum {
@SerializedName("HOME")
HOME("HOME");
private final String value;
CacheEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}

View file

@ -0,0 +1,374 @@
package app.fedilab.android.client.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.sqlite.Sqlite;
public class StatusDraft implements Serializable {
private transient final SQLiteDatabase db;
@SerializedName("id")
public long id = -1;
@SerializedName("instance")
public String instance;
@SerializedName("user_id")
public String user_id;
@SerializedName("statusDraftList")
public List<Status> statusDraftList;
@SerializedName("statusReplyList")
public List<Status> statusReplyList;
@SerializedName("state")
public PostState state;
@SerializedName("created_at")
public Date created_ad;
@SerializedName("updated_at")
public Date updated_at;
@SerializedName("worker_uuid")
public UUID workerUuid;
@SerializedName("scheduled_at")
public Date scheduled_at;
private transient Context context;
public StatusDraft() {
db = null;
}
public StatusDraft(Context context) {
//Creation of the DB with tables
this.context = context;
this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
}
/**
* Serialized a list of Status class
*
* @param statuses List of {@link Status} to serialize
* @return String serialized emoji list
*/
public static String mastodonStatusListToStringStorage(List<Status> statuses) {
Gson gson = new Gson();
try {
return gson.toJson(statuses);
} catch (Exception e) {
return null;
}
}
/**
* Unserialized a Status List
*
* @param serializedStatusList String serialized Status list
* @return List of {@link Status}
*/
public static List<Status> restoreStatusListFromString(String serializedStatusList) {
Gson gson = new Gson();
try {
return gson.fromJson(serializedStatusList, new TypeToken<List<Status>>() {
}.getType());
} catch (Exception e) {
return null;
}
}
/**
* Serialized a list of Status class
*
* @param postState {@link PostState} to serialize
* @return String serialized PostState list
*/
public static String postStateToStringStorage(PostState postState) {
Gson gson = new Gson();
try {
return gson.toJson(postState);
} catch (Exception e) {
return null;
}
}
/**
* Unserialized a PostState
*
* @param serializedPostState String serialized PostState
* @return {@link PostState}
*/
public static PostState restorePostStateFromString(String serializedPostState) {
Gson gson = new Gson();
try {
return gson.fromJson(serializedPostState, PostState.class);
} catch (Exception e) {
return null;
}
}
/**
* Insert statusDraft in db
*
* @param statusDraft {@link StatusDraft}
* @return long - db id
* @throws DBException exception with database
*/
public long insertStatusDraft(StatusDraft statusDraft) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_INSTANCE, statusDraft.instance);
values.put(Sqlite.COL_USER_ID, statusDraft.user_id);
values.put(Sqlite.COL_DRAFTS, mastodonStatusListToStringStorage(statusDraft.statusDraftList));
values.put(Sqlite.COL_REPLIES, mastodonStatusListToStringStorage(statusDraft.statusReplyList));
values.put(Sqlite.COL_CREATED_AT, Helper.dateToString(new Date()));
if (statusDraft.workerUuid != null) {
values.put(Sqlite.COL_WORKER_UUID, ScheduledBoost.uuidToStringStorage(statusDraft.workerUuid));
values.put(Sqlite.COL_SCHEDULED_AT, Helper.dateToString(statusDraft.scheduled_at));
}
//Inserts drafts
try {
return db.insertOrThrow(Sqlite.TABLE_STATUS_DRAFT, null, values);
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Remove a draft from db
*
* @param statusDraft {@link StatusDraft}
* @return int
*/
public int removeDraft(StatusDraft statusDraft) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
return db.delete(Sqlite.TABLE_STATUS_DRAFT, Sqlite.COL_ID + " = '" + statusDraft.id + "'", null);
}
/**
* Remove all drafts for an account from db
*
* @return int
*/
public int removeAllDraft() throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
return db.delete(Sqlite.TABLE_STATUS_DRAFT, Sqlite.COL_USER_ID + " = '" + BaseMainActivity.currentUserID + "' AND " + Sqlite.COL_INSTANCE + " = '" + BaseMainActivity.currentInstance + "'", null);
}
/**
* update statusDraft in db
*
* @param statusDraft {@link StatusDraft}
* @return long - db id
* @throws DBException exception with database
*/
public long updateStatusDraft(StatusDraft statusDraft) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_DRAFTS, mastodonStatusListToStringStorage(statusDraft.statusDraftList));
values.put(Sqlite.COL_REPLIES, mastodonStatusListToStringStorage(statusDraft.statusReplyList));
values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date()));
if (statusDraft.workerUuid != null) {
values.put(Sqlite.COL_WORKER_UUID, ScheduledBoost.uuidToStringStorage(statusDraft.workerUuid));
values.put(Sqlite.COL_SCHEDULED_AT, Helper.dateToString(statusDraft.scheduled_at));
}
//Inserts token
try {
return db.update(Sqlite.TABLE_STATUS_DRAFT,
values, Sqlite.COL_ID + " = ?",
new String[]{String.valueOf(statusDraft.id)});
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* remove schedule statusDraft in db
*
* @param statusDraft {@link StatusDraft}
* @return long - db id
* @throws DBException exception with database
*/
public long removeScheduled(StatusDraft statusDraft) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.putNull(Sqlite.COL_WORKER_UUID);
values.putNull(Sqlite.COL_SCHEDULED_AT);
//Inserts token
try {
return db.update(Sqlite.TABLE_STATUS_DRAFT,
values, Sqlite.COL_ID + " = ?",
new String[]{String.valueOf(statusDraft.id)});
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* update statusDraft in db
*
* @param statusDraft {@link StatusDraft}
* @return long - db id
* @throws DBException exception with database
*/
public long updatePostState(StatusDraft statusDraft) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_STATE, postStateToStringStorage(statusDraft.state));
values.put(Sqlite.COL_UPDATED_AT, Helper.dateToString(new Date()));
//Inserts token
try {
return db.update(Sqlite.TABLE_STATUS_DRAFT,
values, Sqlite.COL_ID + " = ?",
new String[]{String.valueOf(statusDraft.id)});
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Returns the StatusDraft for an account
*
* @param account Account
* @return List<StatusDraft> - List of {@link StatusDraft}
*/
public List<StatusDraft> geStatusDraftList(Account account) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_STATUS_DRAFT, null, Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + account.user_id + "'", null, null, null, Sqlite.COL_UPDATED_AT + " ASC", null);
return cursorToStatusDraftList(c);
} catch (Exception e) {
return null;
}
}
/**
* Returns the StatusDraft for an account that has been scheduled by the client
*
* @param account Account
* @return List<StatusDraft> - List of {@link StatusDraft}
*/
public List<StatusDraft> geStatusDraftScheduledList(Account account) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_STATUS_DRAFT, null, Sqlite.COL_INSTANCE + " = '" + account.instance + "' AND " + Sqlite.COL_USER_ID + " = '" + account.user_id + "' AND " + Sqlite.COL_WORKER_UUID + " != ''", null, null, null, Sqlite.COL_UPDATED_AT + " ASC", null);
return cursorToStatusDraftList(c);
} catch (Exception e) {
return null;
}
}
/**
* Returns the StatusDraft for an account
*
* @param draftId String
* @return StatusDraft - {@link StatusDraft}
*/
public StatusDraft geStatusDraft(String draftId) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_STATUS_DRAFT, null, Sqlite.COL_ID + " = '" + draftId + "'", null, null, null, null, "1");
return convertCursorToStatusDraft(c);
} catch (Exception e) {
return null;
}
}
/**
* Restore statusDraft list from db
*
* @param c Cursor
* @return List<Emoji>
*/
private List<StatusDraft> cursorToStatusDraftList(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
List<StatusDraft> statusDrafts = new ArrayList<>();
while (c.moveToNext()) {
StatusDraft statusDraft = convertCursorToStatusDraft(c);
statusDrafts.add(statusDraft);
}
//Close the cursor
c.close();
return statusDrafts;
}
/**
* Read cursor and hydrate without closing it
*
* @param c - Cursor
* @return Timeline
*/
private StatusDraft convertCursorToStatusDraft(Cursor c) {
StatusDraft statusDraft = new StatusDraft();
statusDraft.id = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_ID));
statusDraft.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE));
statusDraft.user_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID));
statusDraft.statusReplyList = restoreStatusListFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_REPLIES)));
statusDraft.statusDraftList = restoreStatusListFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_DRAFTS)));
statusDraft.state = restorePostStateFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_STATE)));
statusDraft.created_ad = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_CREATED_AT)));
statusDraft.updated_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_UPDATED_AT)));
statusDraft.workerUuid = ScheduledBoost.restoreUuidFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_WORKER_UUID)));
statusDraft.scheduled_at = Helper.stringToDate(context, c.getString(c.getColumnIndexOrThrow(Sqlite.COL_SCHEDULED_AT)));
return statusDraft;
}
}

View file

@ -0,0 +1,419 @@
package app.fedilab.android.client.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.sqlite.Sqlite;
public class Timeline {
private final SQLiteDatabase db;
@SerializedName("id")
public long id;
@SerializedName("user_id")
public String user_id;
@SerializedName("instance")
public String instance;
@SerializedName("position")
public int position;
@SerializedName("type")
public TimeLineEnum type;
@SerializedName("remote_instance")
public String remote_instance;
@SerializedName("displayed")
public boolean displayed;
@SerializedName("timelineOptions")
public TimelineOptions timelineOptions;
private Context context;
public Timeline() {
db = null;
}
public Timeline(Context context) {
//Creation of the DB with tables
this.context = context;
this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
}
/**
* Serialized a TimelineOptions class
*
* @param timelineOptions {@link TimelineOptions} to serialize
* @return String serialized timeline options
*/
public static String timelineOptionsToStringStorage(TimelineOptions timelineOptions) {
Gson gson = new Gson();
try {
return gson.toJson(timelineOptions);
} catch (Exception e) {
return null;
}
}
/**
* Unserialized a TimelineOptions
*
* @param serializedTimelineOptionsString serialized timeline options
* @return {@link TimelineOptions}
*/
public static TimelineOptions restoreTimelineOptionsFromString(String serializedTimelineOptionsString) {
Gson gson = new Gson();
try {
return gson.fromJson(serializedTimelineOptionsString, TimelineOptions.class);
} catch (Exception e) {
return null;
}
}
/**
* Insert a timeline
*
* @param timeline {@link Timeline}
* @return long - db id
* @throws DBException exception with database
*/
public long insert(Timeline timeline) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
if (!canBeModified(timeline)) {
return -1;
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_POSITION, countEntries());
values.put(Sqlite.COL_USER_ID, timeline.user_id);
values.put(Sqlite.COL_INSTANCE, timeline.instance);
values.put(Sqlite.COL_TYPE, timeline.type.getValue());
values.put(Sqlite.COL_REMOTE_INSTANCE, timeline.remote_instance);
values.put(Sqlite.COL_DISPLAYED, timeline.displayed);
if (timeline.timelineOptions != null) {
values.put(Sqlite.COL_TIMELINE_OPTION, timelineOptionsToStringStorage(timeline.timelineOptions));
}
try {
return db.insertOrThrow(Sqlite.TABLE_TIMELINES, null, values);
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
private boolean canBeModified(Timeline timeline) {
return timeline.type != TimeLineEnum.HOME && timeline.type != TimeLineEnum.DIRECT && timeline.type != TimeLineEnum.LOCAL && timeline.type != TimeLineEnum.PUBLIC && timeline.type != TimeLineEnum.NOTIFICATION;
}
/**
* update a timeline
*
* @param timeline {@link Timeline}
* @return long - db id
* @throws DBException exception with database
*/
public long update(Timeline timeline) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_POSITION, timeline.position);
values.put(Sqlite.COL_DISPLAYED, timeline.displayed);
if (timeline.timelineOptions != null && canBeModified(timeline)) {
values.put(Sqlite.COL_TIMELINE_OPTION, timelineOptionsToStringStorage(timeline.timelineOptions));
}
reorderUpdatePosition(timeline);
try {
return db.update(Sqlite.TABLE_TIMELINES,
values, Sqlite.COL_ID + " = ?",
new String[]{String.valueOf(timeline.id)});
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Remove a timeline from db
*
* @param timeline {@link Timeline}
* @return int
*/
public int remove(Timeline timeline) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
if (!canBeModified(timeline)) {
return -1;
}
reorderDeletePosition(timeline);
return db.delete(Sqlite.TABLE_TIMELINES, Sqlite.COL_ID + " = '" + timeline.id + "'", null);
}
/**
* Returns all timelines between two position (positions included)
*
* @return List<Timelines> timelines
*/
public List<Timeline> getTimelineBetweenPosition(int min, int max) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
if (min > max) {
int _t = min;
min = max;
max = min;
}
try {
Cursor c = db.query(Sqlite.TABLE_TIMELINES, null, Sqlite.COL_POSITION + " >= '" + min + "' AND " + Sqlite.COL_POSITION + " <= '" + max + "'", null, null, null, Sqlite.COL_POSITION + " ASC", null);
return cursorToListTimelines(c);
} catch (Exception e) {
return null;
}
}
/**
* Returns all timelines after a position (position included)
*
* @return List<Timelines> timelines
*/
public List<Timeline> getTimelineAfterPosition(int position) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_TIMELINES, null, Sqlite.COL_POSITION + " > '" + position + "'", null, null, null, Sqlite.COL_POSITION + " ASC", null);
return cursorToListTimelines(c);
} catch (Exception e) {
return null;
}
}
/**
* Reorder each position after moving an element
*
* @param _mTimeline Timeline
* @throws DBException - db exception
*/
public void reorderUpdatePosition(Timeline _mTimeline) throws DBException {
Timeline previousPosition = getTimeline(_mTimeline.id);
List<Timeline> timelines = getTimelineBetweenPosition(_mTimeline.position, previousPosition.position);
if (previousPosition.position > _mTimeline.position) {
for (int i = _mTimeline.position; i < timelines.size(); i++) {
Timeline timeline = timelines.get(i);
timeline.position++;
update(timeline);
}
} else if (previousPosition.position < _mTimeline.position) {
for (int i = previousPosition.position + 1; i <= timelines.size(); i++) {
Timeline timeline = timelines.get(i);
timeline.position--;
update(timeline);
}
}
}
/**
* Reorder each position after deleting an element
*
* @param _mTimeline Timeline
* @throws DBException - db exception
*/
public void reorderDeletePosition(Timeline _mTimeline) throws DBException {
List<Timeline> timelines = getTimelineAfterPosition(_mTimeline.position);
for (Timeline timeline : timelines) {
timeline.position--;
update(timeline);
}
}
/**
* Returns all timelines
*
* @return List<Timelines> timelines
*/
public List<Timeline> getTimelines() throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_TIMELINES, null, null, null, null, null, Sqlite.COL_POSITION + " ASC", null);
return cursorToListTimelines(c);
} catch (Exception e) {
return null;
}
}
/**
* Returns a timeline
*
* @return Timelines timeline
*/
public Timeline getTimeline(long id) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_TIMELINES, null, Sqlite.COL_ID + "='" + id + "'", null, null, null, Sqlite.COL_POSITION + " ASC", null);
return cursorToTimeline(c);
} catch (Exception e) {
return null;
}
}
private List<Timeline> cursorToListTimelines(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
List<Timeline> timelineList = new ArrayList<>();
while (c.moveToNext()) {
Timeline timeline = convertCursorToTimeLine(c);
timelineList.add(timeline);
}
//Close the cursor
c.close();
return timelineList;
}
private Timeline cursorToTimeline(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
//Take the first element
c.moveToFirst();
Timeline timeline = convertCursorToTimeLine(c);
//Close the cursor
c.close();
return timeline;
}
/**
* Read cursor and hydrate without closing it
*
* @param c - Cursor
* @return Timeline
*/
private Timeline convertCursorToTimeLine(Cursor c) {
Timeline timeline = new Timeline();
timeline.id = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_ID));
timeline.timelineOptions = restoreTimelineOptionsFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_TIMELINE_OPTION)));
timeline.type = TimeLineEnum.valueOf(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_TYPE)));
timeline.displayed = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_TIMELINE_OPTION)) == 1;
timeline.instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INSTANCE));
timeline.user_id = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_USER_ID));
timeline.remote_instance = c.getString(c.getColumnIndexOrThrow(Sqlite.COL_REMOTE_INSTANCE));
timeline.position = c.getInt(c.getColumnIndexOrThrow(Sqlite.COL_POSITION));
return timeline;
}
/**
* Count entry in db
*
* @return int - number of timelines recorded in db
* @throws DBException Exception
*/
public int countEntries() throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_TIMELINES, null);
mCount.moveToFirst();
int count = mCount.getInt(0);
mCount.close();
return count;
}
public enum TimeLineEnum {
@SerializedName("HOME")
HOME("HOME"),
@SerializedName("DIRECT")
DIRECT("DIRECT"),
@SerializedName("NOTIFICATION")
NOTIFICATION("NOTIFICATION"),
@SerializedName("LOCAL")
LOCAL("LOCAL"),
@SerializedName("PUBLIC")
PUBLIC("PUBLIC"),
@SerializedName("TAG")
TAG("TAG"),
@SerializedName("LIST")
LIST("LIST"),
@SerializedName("REMOTE")
REMOTE("REMOTE"),
@SerializedName("ACCOUNT_TIMELINE")
ACCOUNT_TIMELINE("ACCOUNT_TIMELINE"),
@SerializedName("MUTED_TIMELINE")
MUTED_TIMELINE("MUTED_TIMELINE"),
@SerializedName("BOOKMARK_TIMELINE")
BOOKMARK_TIMELINE("BOOKMARK_TIMELINE"),
@SerializedName("BLOCKED_TIMELINE")
BLOCKED_TIMELINE("BLOCKED_TIMELINE"),
@SerializedName("FAVOURITE_TIMELINE")
FAVOURITE_TIMELINE("FAVOURITE_TIMELINE"),
@SerializedName("REBLOG_TIMELINE")
REBLOG_TIMELINE("REBLOG_TIMELINE"),
@SerializedName("SCHEDULED_TOOT_SERVER")
SCHEDULED_TOOT_SERVER("SCHEDULED_TOOT_SERVER"),
@SerializedName("SCHEDULED_TOOT_CLIENT")
SCHEDULED_TOOT_CLIENT("SCHEDULED_TOOT_CLIENT"),
@SerializedName("SCHEDULED_BOOST")
SCHEDULED_BOOST("SCHEDULED_BOOST");
private final String value;
TimeLineEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
public static class TimelineOptions {
@SerializedName("all")
public List<String> all;
@SerializedName("any")
public List<String> any;
@SerializedName("none")
public List<String> none;
@SerializedName("data")
public List<String> data;
@SerializedName("media_only")
public boolean media_only;
@SerializedName("sensitive")
public boolean sensitive;
@SerializedName("list_id")
public String list_id;
}
}

View file

@ -0,0 +1,78 @@
package app.fedilab.android.client.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class WellKnownNodeinfo {
@SerializedName("links")
public List<NodeInfoLinks> links;
public static class NodeInfoLinks {
@SerializedName("reel")
public String reel;
@SerializedName("href")
public String href;
}
public static class NodeInfo {
@SerializedName("version")
public String version;
@SerializedName("software")
public Software software;
@SerializedName("usage")
public Usage usage;
@SerializedName("metadata")
public Metadata metadata;
@SerializedName("openRegistrations")
public boolean openRegistrations;
}
public static class Software {
@SerializedName("name")
public String name;
@SerializedName("version")
public String version;
}
public static class Usage {
@SerializedName("users")
public Users users;
@SerializedName("localPosts")
public int localPosts;
}
public static class Users {
@SerializedName("total")
public int total;
@SerializedName("activeMonth")
public int activeMonth;
@SerializedName("activeHalfyear")
public int activeHalfyear;
}
public static class Metadata {
@SerializedName("nodeName")
public String nodeName;
@SerializedName("nodeDescription")
public String nodeDescription;
@SerializedName("staffAccounts")
public List<String> staffAccounts;
}
}

View file

@ -0,0 +1,49 @@
package app.fedilab.android.client.entities.app;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import app.fedilab.android.client.entities.Timeline;
import app.fedilab.android.client.mastodon.entities.MastodonList;
public class PinnedTimeline implements Serializable {
@SerializedName("id")
public int id;
@SerializedName("userId")
public String userId;
@SerializedName("instance")
public String instance;
@SerializedName("position")
public int position;
@SerializedName("displayed")
public boolean displayed = true;
@SerializedName("type")
public Timeline.TimeLineEnum type;
@SerializedName("remoteInstance")
public RemoteInstance remoteInstance;
@SerializedName("tagTimeline")
public TagTimeline tagTimeline;
@SerializedName("mastodonList")
public MastodonList mastodonList;
@SerializedName("currentFilter")
public String currentFilter;
public transient boolean isSelected = false;
}

View file

@ -0,0 +1,62 @@
package app.fedilab.android.client.entities.app;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.List;
public class RemoteInstance implements Serializable {
@SerializedName("dbID")
public long dbID;
@SerializedName("id")
public String id;
@SerializedName("host")
public String host;
@SerializedName("type")
public InstanceType type;
@SerializedName("tags")
public List<String> tags;
@SerializedName("filteredWith")
public String filteredWith;
public enum InstanceType {
@SerializedName("MASTODON")
MASTODON("MASTODON"),
@SerializedName("PIXELFED")
PIXELFED("PIXELFED"),
@SerializedName("PEERTUBE")
PEERTUBE("PEERTUBE"),
@SerializedName("NITTER")
NITTER("NITTER"),
@SerializedName("MISSKEY")
MISSKEY("MISSKEY"),
@SerializedName("GNU")
GNU("GNU");
private final String value;
InstanceType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}

View file

@ -0,0 +1,40 @@
package app.fedilab.android.client.entities.app;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.List;
public class TagTimeline implements Serializable {
@SerializedName("id")
public int id;
@SerializedName("name")
public String name;
@SerializedName("displayName")
public String displayName;
@SerializedName("isART")
public boolean isART;
@SerializedName("isNSFW")
public boolean isNSFW;
@SerializedName("any")
public List<String> any;
@SerializedName("all")
public List<String> all;
@SerializedName("none")
public List<String> none;
}

View file

@ -0,0 +1,34 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.List;
import app.fedilab.android.client.mastodon.entities.JoinMastodonInstance;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface JoinMastodonService {
@GET("servers")
Call<List<JoinMastodonInstance>> getInstances(
@Query("category") String category
);
}

View file

@ -0,0 +1,447 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.List;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.client.mastodon.entities.FeaturedTag;
import app.fedilab.android.client.mastodon.entities.Filter;
import app.fedilab.android.client.mastodon.entities.IdentityProof;
import app.fedilab.android.client.mastodon.entities.MastodonList;
import app.fedilab.android.client.mastodon.entities.Preferences;
import app.fedilab.android.client.mastodon.entities.RelationShip;
import app.fedilab.android.client.mastodon.entities.Report;
import app.fedilab.android.client.mastodon.entities.Status;
import app.fedilab.android.client.mastodon.entities.Tag;
import app.fedilab.android.client.mastodon.entities.Token;
import okhttp3.MultipartBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.Multipart;
import retrofit2.http.PATCH;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Part;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface MastodonAccountsService {
/*
* Accounts
*/
//Register account
@FormUrlEncoded
@POST("accounts")
Call<Token> registerAccount(
@Header("Authorization") String app_token,
@Field("username") String username,
@Field("email") String email,
@Field("password") String password,
@Field("agreement") boolean agreement,
@Field("locale") String locale,
@Field("reason") String reason);
//Info about the connected account
@GET("accounts/verify_credentials")
Call<Account> verify_credentials(
@Header("Authorization") String token);
@Multipart
@PATCH("accounts/update_credentials")
Call<Account> update_media(
@Header("Authorization") String token,
@Part MultipartBody.Part avatar,
@Part MultipartBody.Part header
);
@Headers({"Accept: application/json"})
@PATCH("accounts/update_credentials")
Call<Account> update_credentials(
@Header("Authorization") String token, @Body Account.AccountParams accountParams
);
@FormUrlEncoded
@PATCH("accounts/update_credentials")
Call<Account> update_credentials(
@Header("Authorization") String token,
@Field("discoverable") Boolean discoverable,
@Field("bot") Boolean bot,
@Field("display_name") String display_name,
@Field("note") String note,
@Field("locked") Boolean locked,
@Field("source[privacy]") String privacy,
@Field("source[sensitive]") Boolean sensitive,
@Field("source[language]") String language,
@Field("fields_attributes") List<app.fedilab.android.client.mastodon.entities.Field.FieldParams> fields
);
//Get Account
@GET("accounts/{id}")
Call<Account> getAccount(
@Header("Authorization") String token,
@Path("id") String id
);
//Get Account statuses
@GET("accounts/{id}/statuses")
Call<List<Status>> getAccountStatuses(
@Header("Authorization") String token,
@Path("id") String id,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("exclude_replies") Boolean exclude_replies,
@Query("exclude_reblogs") Boolean exclude_reblogs,
@Query("only_media") Boolean only_media,
@Query("pinned") Boolean pinned,
@Query("limit") int limit
);
//Get Account followers
@GET("accounts/{id}/followers")
Call<List<Account>> getAccountFollowers(
@Header("Authorization") String token,
@Path("id") String id,
@Query("max_id") String max_id,
@Query("since_id") String since_id
);
//Get Account following
@GET("accounts/{id}/following")
Call<List<Account>> getAccountFollowing(
@Header("Authorization") String token,
@Path("id") String id,
@Query("max_id") String max_id,
@Query("since_id") String since_id
);
//Get Account featured tags
@GET("accounts/{id}/featured_tags")
Call<List<FeaturedTag>> getAccountFeaturedTags(
@Header("Authorization") String token,
@Path("id") String id
);
//Lists containing this account
@GET("accounts/{id}/lists")
Call<List<MastodonList>> getListContainingAccount(
@Header("Authorization") String token,
@Path("id") String id
);
//Get Identity proofs
@GET("accounts/{id}/identity_proofs")
Call<List<IdentityProof>> getIdentityProofs(
@Header("Authorization") String token,
@Path("id") String id
);
//Follow account
@FormUrlEncoded
@POST("accounts/{id}/follow")
Call<RelationShip> follow(
@Header("Authorization") String app_token,
@Path("id") String id,
@Field("reblogs") boolean reblogs,
@Field("notify") boolean notify
);
//Follow account
@FormUrlEncoded
@POST("accounts/{id}/note")
Call<RelationShip> note(
@Header("Authorization") String app_token,
@Path("id") String id,
@Field("comment") boolean comment
);
//Unfollow account
@POST("accounts/{id}/unfollow")
Call<RelationShip> unfollow(
@Header("Authorization") String app_token,
@Path("id") String id
);
//Block account
@POST("accounts/{id}/block")
Call<RelationShip> block(
@Header("Authorization") String app_token,
@Path("id") String id
);
//Unblock account
@POST("accounts/{id}/unblock")
Call<RelationShip> unblock(
@Header("Authorization") String app_token,
@Path("id") String id
);
//Mute account
@FormUrlEncoded
@POST("accounts/{id}/mute")
Call<RelationShip> mute(
@Header("Authorization") String app_token,
@Path("id") String id,
@Field("notifications") Boolean notifications,
@Field("duration") Integer duration
);
//Unmute account
@POST("accounts/{id}/unmute")
Call<RelationShip> unmute(
@Header("Authorization") String app_token,
@Path("id") String id
);
//Feature on profile
@POST("accounts/{id}/pin")
Call<RelationShip> endorse(
@Header("Authorization") String app_token,
@Path("id") String id
);
//Unfeature account
@POST("accounts/{id}/unpin")
Call<RelationShip> unendorse(
@Header("Authorization") String app_token,
@Path("id") String id
);
//User note
@FormUrlEncoded
@POST("accounts/{id}/note")
Call<RelationShip> note(
@Header("Authorization") String app_token,
@Path("id") String id,
@Field("comment") String comment
);
//Get relationships
@GET("accounts/relationships")
Call<List<RelationShip>> getRelationships(
@Header("Authorization") String token,
@Query("id[]") List<String> ids
);
//Get search
@GET("accounts/search")
Call<List<Account>> searchAccounts(
@Header("Authorization") String token,
@Query("q") String q,
@Query("limit") int limit,
@Query("resolve") boolean resolve,
@Query("following") boolean following
);
//Bookmarks
@GET("bookmarks")
Call<List<Status>> getBookmarks(
@Header("Authorization") String token,
@Query("limit") String limit,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id
);
//favourites
@GET("favourites")
Call<List<Status>> getFavourites(
@Header("Authorization") String token,
@Query("limit") String limit,
@Query("min_id") String min_id,
@Query("max_id") String max_id
);
//muted users
@GET("mutes")
Call<List<Account>> getMutes(
@Header("Authorization") String token,
@Query("limit") String limit,
@Query("max_id") String max_id,
@Query("since_id") String since_id
);
//Blocked users
@GET("blocks")
Call<List<Account>> getBlocks(
@Header("Authorization") String token,
@Query("limit") String limit,
@Query("max_id") String max_id,
@Query("since_id") String since_id
);
//Get blocked domains
@GET("domain_blocks")
Call<List<String>> getDomainBlocks(
@Header("Authorization") String token,
@Query("limit") String limit,
@Query("max_id") String max_id,
@Query("since_id") String since_id
);
//Add a blocked domains
@FormUrlEncoded
@POST("domain_blocks")
Call<Void> addDomainBlock(
@Header("Authorization") String token,
@Field("domain") String domain
);
//Remove a blocked domains
@DELETE("domain_blocks")
Call<Void> removeDomainBlocks(
@Header("Authorization") String token,
@Field("domain") String domain
);
//Get filters
@GET("filters")
Call<List<Filter>> getFilters(
@Header("Authorization") String token);
//Get a filter with its id
@GET("filters/{id}")
Call<Filter> getFilter(
@Header("Authorization") String token,
@Path("id") String id);
//Add a filter
@FormUrlEncoded
@POST("filters")
Call<Filter> addFilter(
@Header("Authorization") String token,
@Field("phrase") String phrase,
@Field("context[]") List<String> context,
@Field("irreversible") boolean irreversible,
@Field("whole_word") boolean whole_word,
@Field("expires_in") long expires_in
);
//Edit a filter
@FormUrlEncoded
@PUT("filters/{id}")
Call<Filter> editFilter(
@Header("Authorization") String token,
@Path("id") String id,
@Field("phrase") String phrase,
@Field("context[]") List<String> context,
@Field("irreversible") boolean irreversible,
@Field("whole_word") boolean whole_word,
@Field("expires_in") long expires_in
);
//Remove a filter
@DELETE("filters/{id}")
Call<Void> removeFilter(
@Header("Authorization") String token,
@Path("id") String id
);
//Post a report
@Headers({"Accept: application/json"})
@POST("reports")
Call<Report> report(
@Header("Authorization") String token, @Body Report.ReportParams params
);
//Get follow request
@GET("follow_requests")
Call<List<Account>> getFollowRequests(
@Header("Authorization") String token,
@Path("limit") String limit);
//Accept follow request
@POST("follow_requests/{id}/authorize")
Call<RelationShip> acceptFollow(
@Header("Authorization") String token,
@Path("id") String id
);
//Reject follow request
@POST("follow_requests/{id}/reject")
Call<RelationShip> rejectFollow(
@Header("Authorization") String token,
@Path("id") String id
);
//Accounts that the user is currently featuring on their profile.
@GET("endorsements")
Call<List<Account>> getEndorsements(
@Header("Authorization") String token,
@Query("limit") String limit,
@Query("max_id") String max_id,
@Query("since_id") String since_id
);
//Feature tags
@GET("featured_tags")
Call<List<FeaturedTag>> getFeaturedTags(
@Header("Authorization") String token
);
//Add a feature tags
@FormUrlEncoded
@POST("featured_tags")
Call<FeaturedTag> addFeaturedTag(
@Header("Authorization") String token,
@Field("name") String name
);
//Remove a feature tags
@DELETE("featured_tags/{id}")
Call<Void> removeFeaturedTag(
@Header("Authorization") String token,
@Path("id") String id
);
//Feature tags suggestions
@GET("featured_tags/suggestions")
Call<List<Tag>> getFeaturedTagsSuggestions(
@Header("Authorization") String token
);
//Get user preferences
@GET("preferences")
Call<Preferences> getPreferences(
@Header("Authorization") String token
);
//Get user suggestions
@GET("suggestions")
Call<List<Account>> getSuggestions(
@Header("Authorization") String token,
@Query("limit") String limit
);
//Remove a user suggestion
@DELETE("suggestions/{account_id}")
Call<Void> removeSuggestion(
@Header("Authorization") String token,
@Path("account_id") String account_id
);
}

View file

@ -0,0 +1,149 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.List;
import app.fedilab.android.client.mastodon.entities.AdminAccount;
import app.fedilab.android.client.mastodon.entities.AdminReport;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface MastodonAdminService {
@GET("/admin/accounts")
Call<List<AdminAccount>> getAccounts(
@Header("Authorization") String token,
@Query("local") boolean local,
@Query("remote") boolean remote,
@Query("by_domain") String by_domain,
@Query("active") boolean active,
@Query("pending") boolean pending,
@Query("disabled") boolean disabled,
@Query("silenced") boolean silenced,
@Query("suspended") boolean suspended,
@Query("username") String username,
@Query("display_name") String display_name,
@Query("email") String email,
@Query("ip") String ip,
@Query("staff") boolean staff,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("limit") int limit
);
@GET("/admin/accounts/{id}")
Call<AdminAccount> getAccount(
@Header("Authorization") String token,
@Path("id") String id
);
@POST("/admin/accounts/{account_id}/action")
Call<Void> performAction(
@Header("Authorization") String app_token,
@Path("account_id") String account_id,
@Field("type") String type,
@Field("report_id") String report_id,
@Field("warning_preset_id") String warning_preset_id,
@Field("text") String text,
@Field("send_email_notification") boolean send_email_notification
);
@FormUrlEncoded
@POST("/admin/accounts/{account_id}/approve")
Call<AdminAccount> approve(
@Header("Authorization") String app_token,
@Path("account_id") String account_id
);
@FormUrlEncoded
@POST("/admin/accounts/{account_id}/reject")
Call<AdminAccount> reject(
@Header("Authorization") String app_token,
@Path("account_id") String account_id
);
@FormUrlEncoded
@POST("/admin/accounts/{account_id}/enable")
Call<AdminAccount> enable(
@Header("Authorization") String app_token,
@Path("account_id") String account_id
);
@FormUrlEncoded
@POST("/admin/accounts/{account_id}/unsilence")
Call<AdminAccount> unsilence(
@Header("Authorization") String app_token,
@Path("account_id") String account_id
);
@FormUrlEncoded
@POST("/admin/accounts/{account_id}/unsuspend")
Call<AdminAccount> unsuspend(
@Header("Authorization") String app_token,
@Path("account_id") String account_id
);
@FormUrlEncoded
@GET("/admin/reports")
Call<List<AdminReport>> getReports(
@Header("Authorization") String token,
@Field("resolved") boolean resolved,
@Field("account_id") String account_id,
@Field("target_account_id") String target_account_id
);
@FormUrlEncoded
@GET("/admin/reports/{id}")
Call<AdminReport> getReport(
@Header("Authorization") String token,
@Path("id") String id
);
@FormUrlEncoded
@POST("/admin/reports/{id}/assign_to_self")
Call<AdminReport> assignToSelf(
@Header("Authorization") String app_token,
@Path("id") String id
);
@FormUrlEncoded
@POST("/admin/reports/{id}/unassign")
Call<AdminReport> unassign(
@Header("Authorization") String app_token,
@Path("id") String id
);
@FormUrlEncoded
@POST("/admin/reports/{id}/resolve")
Call<AdminReport> resolved(
@Header("Authorization") String app_token,
@Path("id") String id
);
@FormUrlEncoded
@POST("/admin/reports/{id}/reopen")
Call<AdminReport> reopen(
@Header("Authorization") String app_token,
@Path("id") String id
);
}

View file

@ -0,0 +1,61 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.List;
import app.fedilab.android.client.mastodon.entities.Announcement;
import retrofit2.Call;
import retrofit2.http.DELETE;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface MastodonAnnouncementsService {
@GET("/announcements")
Call<List<Announcement>> getAnnouncements(
@Header("Authorization") String token,
@Query("with_dismissed") boolean with_dismissed
);
@FormUrlEncoded
@POST("/announcements/{id}/dismiss")
Call<Void> dismiss(
@Header("Authorization") String app_token,
@Path("id") String id
);
@FormUrlEncoded
@PUT("/announcements/{id}/reactions/{name}")
Call<Void> addReaction(
@Header("Authorization") String app_token,
@Path("id") String id,
@Path("name") String name
);
@DELETE("/announcements/{id}/reactions/{name}")
Call<Void> removeReaction(
@Header("Authorization") String app_token,
@Path("id") String id,
@Path("name") String name
);
}

View file

@ -0,0 +1,66 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import app.fedilab.android.client.mastodon.entities.App;
import app.fedilab.android.client.mastodon.entities.Token;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.POST;
public interface MastodonAppsService {
/*
* OAUTH - TOKEN
*/
//Create app
@FormUrlEncoded
@POST("apps")
Call<App> createApp(
@Field("client_name") String client_name,
@Field("redirect_uris") String redirect_uris,
@Field("scopes") String scopes,
@Field("website") String website);
@GET("apps/verify_credentials")
Call<App> verifyCredentials(
@Header("Authorization") String app_token);
//Create token
@FormUrlEncoded
@POST("oauth/token")
Call<Token> createToken(
@Field("grant_type") String grant_type,
@Field("client_id") String client_id,
@Field("client_secret") String client_secret,
@Field("redirect_uri") String redirect_uri,
@Field("scope") String scope,
@Field("code") String code);
//Revoke token
@FormUrlEncoded
@POST("oauth/revoke")
Call<Void> revokeToken(
@Field("client_id") String client_id,
@Field("client_secret") String client_secret,
@Field("token") String token);
}

View file

@ -0,0 +1,54 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.List;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.client.mastodon.entities.Activity;
import app.fedilab.android.client.mastodon.entities.Emoji;
import app.fedilab.android.client.mastodon.entities.Instance;
import app.fedilab.android.client.mastodon.entities.Tag;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface MastodonInstanceService {
@GET("instance")
Call<Instance> instance();
@GET("instance/peers")
Call<List<String>> connectedInstance();
@GET("instance/activity")
Call<List<Activity>> weeklyActivity();
@GET("trends")
Call<List<Tag>> trends();
@GET("directory")
Call<List<Account>> directory(
@Query("offset") int offset,
@Query("limit") int limit,
@Query("order") String order,
@Query("local") boolean local
);
@GET("custom_emojis")
Call<List<Emoji>> customEmoji();
}

View file

@ -0,0 +1,98 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.List;
import app.fedilab.android.client.mastodon.entities.Notification;
import app.fedilab.android.client.mastodon.entities.PushSubscription;
import retrofit2.Call;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface MastodonNotificationsService {
@GET("notifications")
Call<List<Notification>> getNotifications(
@Header("Authorization") String token,
@Query("exclude_types[]") List<String> exclude_types,
@Query("account_id") String account_id,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("limit") int limit
);
@GET("notifications/{id}")
Call<Notification> getNotification(
@Header("Authorization") String token,
@Path("id") String id
);
@POST("notifications/clear")
Call<Void> clearAllNotifications(
@Header("Authorization") String token
);
@FormUrlEncoded
@POST("notifications/{id}/dismiss")
Call<Void> dismissNotification(
@Header("Authorization") String token,
@Path("id") String id
);
@FormUrlEncoded
@POST("push/subscription")
Call<PushSubscription> pushSubscription(
@Header("Authorization") String token,
@Field("subscription[endpoint]") String endpoint,
@Field("subscription[keys][p256dh]") String keys_p256dh,
@Field("subscription[keys][auth]") String keys_auth,
@Field("data[alerts][follow]") boolean follow,
@Field("data[alerts][favourite]") boolean favourite,
@Field("data[alerts][reblog]") boolean reblog,
@Field("data[alerts][mention]") boolean mention,
@Field("data[alerts][poll]") boolean poll
);
@GET("push/subscription")
Call<PushSubscription> getPushSubscription(
@Header("Authorization") String token
);
@FormUrlEncoded
@PUT("push/subscription")
Call<PushSubscription> updatePushSubscription(
@Header("Authorization") String token,
@Field("data[alerts][follow]") boolean follow,
@Field("data[alerts][favourite]") boolean favourite,
@Field("data[alerts][reblog]") boolean reblog,
@Field("data[alerts][mention]") boolean mention,
@Field("data[alerts][poll]") boolean poll
);
@DELETE("push/subscription")
Call<Void> deletePushsubscription(
@Header("Authorization") String token
);
}

View file

@ -0,0 +1,31 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import app.fedilab.android.client.mastodon.entities.Oembed;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface MastodonOembedService {
@GET("/oembed")
Call<Oembed> oembed(
@Query("url") String url,
@Query("maxwidth") int maxwidth,
@Query("maxheight") int maxheight
);
}

View file

@ -0,0 +1,40 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import app.fedilab.android.client.mastodon.entities.Results;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Query;
public interface MastodonSearchService {
//API V2
@GET("search")
Call<Results> search(
@Header("Authorization") String token,
@Query("q") String q,
@Query("account_id") String account_id,
@Query("type") String type,
@Query("exclude_unreviewed") boolean exclude_unreviewed,
@Query("resolve") boolean resolve,
@Query("following") boolean following,
@Query("offset") int offset,
@Query("max_id") String max_id,
@Query("min_id") String min_id,
@Query("limit") int limit
);
}

View file

@ -0,0 +1,284 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.Date;
import java.util.List;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.client.mastodon.entities.Attachment;
import app.fedilab.android.client.mastodon.entities.Card;
import app.fedilab.android.client.mastodon.entities.Context;
import app.fedilab.android.client.mastodon.entities.Poll;
import app.fedilab.android.client.mastodon.entities.ScheduledStatus;
import app.fedilab.android.client.mastodon.entities.Status;
import okhttp3.MultipartBody;
import retrofit2.Call;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Part;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface MastodonStatusesService {
//Post a status
@FormUrlEncoded
@POST("statuses")
Call<Status> createStatus(
@Header("Idempotency-Key") String idempotency_Key,
@Header("Authorization") String token,
@Field("status") String status,
@Field("media_ids[]") List<String> media_ids,
@Field("poll[options][]") List<String> poll_options,
@Field("poll[expires_in]") Integer poll_expire_in,
@Field("poll[multiple]") Boolean poll_multiple,
@Field("poll[hide_totals]") Boolean poll_hide_totals,
@Field("in_reply_to_id") String in_reply_to_id,
@Field("sensitive") Boolean sensitive,
@Field("spoiler_text") String spoiler_text,
@Field("visibility") String visibility,
@Field("language") String language
);
//Post a scheduled status
@FormUrlEncoded
@POST("statuses")
Call<ScheduledStatus> createScheduledStatus(
@Header("Idempotency-Key") String idempotency_Key,
@Header("Authorization") String token,
@Field("status") String status,
@Field("media_ids[]") List<String> media_ids,
@Field("poll[options][]") List<String> poll_options,
@Field("poll[expires_in]") Integer poll_expire_in,
@Field("poll[multiple]") Boolean poll_multiple,
@Field("poll[hide_totals]") Boolean poll_hide_totals,
@Field("in_reply_to_id") String in_reply_to_id,
@Field("sensitive") Boolean sensitive,
@Field("spoiler_text") String spoiler_text,
@Field("visibility") String visibility,
@Field("scheduled_at") String scheduled_at,
@Field("language") String language
);
//Get a specific status
@GET("statuses/{id}")
Call<Status> getStatus(
@Header("Authorization") String token,
@Path("id") String id
);
//Delete a specific status
@DELETE("statuses/{id}")
Call<Status> deleteStatus(
@Header("Authorization") String token,
@Path("id") String id
);
//Get parent and child statuses
@GET("statuses/{id}/context")
Call<Context> getContext(
@Header("Authorization") String token,
@Path("id") String id
);
//Get reblogged by
@GET("statuses/{id}/reblogged_by")
Call<List<Account>> getRebloggedBy(
@Header("Authorization") String token,
@Path("id") String id,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("limit") int limit
);
//Get favourited by
@GET("statuses/{id}/favourited_by")
Call<List<Account>> getFavourited(
@Header("Authorization") String token,
@Path("id") String id,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("limit") int limit
);
//Add status to favourites
@POST("statuses/{id}/favourite")
Call<Status> favourites(
@Header("Authorization") String token,
@Path("id") String id
);
//Remove status from favourites
@POST("statuses/{id}/unfavourite")
Call<Status> unFavourite(
@Header("Authorization") String token,
@Path("id") String id
);
//Reblog a status
@FormUrlEncoded
@POST("statuses/{id}/reblog")
Call<Status> reblog(
@Header("Authorization") String token,
@Path("id") String id,
@Field("visibility") String visibility
);
//Unreblog a status
@POST("statuses/{id}/unreblog")
Call<Status> unReblog(
@Header("Authorization") String token,
@Path("id") String id
);
//Bookmark a status
@POST("statuses/{id}/bookmark")
Call<Status> bookmark(
@Header("Authorization") String token,
@Path("id") String id
);
//Unbookmark a status
@POST("statuses/{id}/unbookmark")
Call<Status> unBookmark(
@Header("Authorization") String token,
@Path("id") String id
);
//Mute a conversation
@POST("statuses/{id}/mute")
Call<Status> muteConversation(
@Header("Authorization") String token,
@Path("id") String id
);
//UnMute a conversation
@POST("statuses/{id}/unmute")
Call<Status> unMuteConversation(
@Header("Authorization") String token,
@Path("id") String id
);
//Pin a status
@POST("statuses/{id}/pin")
Call<Status> pin(
@Header("Authorization") String token,
@Path("id") String id
);
//UNPin a status
@POST("statuses/{id}/unpin")
Call<Status> unPin(
@Header("Authorization") String token,
@Path("id") String id
);
//Get reblogged by
@GET("statuses/{id}/card")
Call<Card> getCard(
@Header("Authorization") String token,
@Path("id") String id
);
//Get a Media
@GET("media/{id}")
Call<Attachment> getMedia(
@Header("Authorization") String token,
@Path("id") String id
);
//Upload a Media
@Multipart
@POST("media")
Call<Attachment> postMedia(
@Header("Authorization") String token,
@Part MultipartBody.Part file,
@Part MultipartBody.Part thumbnail,
@Part("description") String description,
@Part("focus") String focus
);
//Edit a Media
@Multipart
@PUT("media/{id}")
Call<Attachment> updateMedia(
@Header("Authorization") String token,
@Path("id") String id,
@Part MultipartBody.Part file,
@Part MultipartBody.Part thumbnail,
@Part("description") String description,
@Part("focus") String focus
);
//Get a Poll
@GET("polls/{id}")
Call<Poll> getPoll(
@Header("Authorization") String token,
@Path("id") String id
);
//Vote on a Poll
@FormUrlEncoded
@POST("polls/{id}/votes")
Call<Poll> votePoll(
@Header("Authorization") String token,
@Path("id") String id,
@Field("choices[]") int[] choices
);
//Get scheduled statuses
@GET("scheduled_statuses")
Call<List<ScheduledStatus>> getScheduledStatuses(
@Header("Authorization") String token,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("limit") int limit
);
//Get scheduled status
@GET("scheduled_statuses/{id}")
Call<ScheduledStatus> getScheduledStatus(
@Header("Authorization") String token,
@Path("id") String id
);
//Schedule a status
@FormUrlEncoded
@PUT("scheduled_statuses/{id}")
Call<ScheduledStatus> updateScheduleStatus(
@Header("Authorization") String token,
@Path("id") String id,
@Field("scheduled_at") Date scheduled_at
);
//Delete a scheduled status
@DELETE("scheduled_statuses/{id}")
Call<Void> deleteScheduledStatus(
@Header("Authorization") String token,
@Path("id") String id
);
}

View file

@ -0,0 +1,194 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.List;
import app.fedilab.android.client.mastodon.entities.Account;
import app.fedilab.android.client.mastodon.entities.Conversation;
import app.fedilab.android.client.mastodon.entities.Marker;
import app.fedilab.android.client.mastodon.entities.MastodonList;
import app.fedilab.android.client.mastodon.entities.Status;
import retrofit2.Call;
import retrofit2.http.DELETE;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface MastodonTimelinesService {
//Public timelines
@GET("timelines/public")
Call<List<Status>> getPublic(
@Header("Authorization") String token,
@Query("local") boolean local,
@Query("remote") boolean remote,
@Query("only_media") boolean only_media,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("limit") int limit
);
//Public Tags timelines
@GET("timelines/tag/{hashtag}")
Call<List<Status>> getHashTag(
@Header("Authorization") String token,
@Path("hashtag") String hashtag,
@Query("local") boolean local,
@Query("only_media") boolean only_media,
@Query("all[]") List<String> all,
@Query("any[]") List<String> any,
@Query("none[]") List<String> none,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("limit") int limit
);
//Home timeline
@GET("timelines/home")
Call<List<Status>> getHome(
@Header("Authorization") String token,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("limit") int limit,
@Query("local") boolean local
);
//List timeline
@GET("timelines/list/{list_id}")
Call<List<Status>> getList(
@Header("Authorization") String token,
@Path("list_id") String list_id,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("limit") int limit
);
//get conversations
@GET("conversations")
Call<List<Conversation>> getConversations(
@Header("Authorization") String token,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("min_id") String min_id,
@Query("limit") int limit
);
//Delete a conversation
@DELETE("conversations/{id}")
Call<Void> deleteConversation(
@Header("Authorization") String token,
@Path("id") String id
);
//Mark a conversation as read
@FormUrlEncoded
@POST("conversations/{id}/read")
Call<Status> markReadConversation(
@Header("Authorization") String token,
@Path("id") String id
);
//Show user list
@GET("lists")
Call<List<MastodonList>> getLists(
@Header("Authorization") String token
);
//Get Single list
@GET("lists/{id}")
Call<MastodonList> getList(
@Header("Authorization") String token,
@Path("id") String id
);
//Create a user list
@FormUrlEncoded
@POST("lists")
Call<MastodonList> createList(
@Header("Authorization") String token,
@Field("title") String title,
@Field("replies_policy") String replies_policy
);
//Update a list
@FormUrlEncoded
@PUT("lists/{id}")
Call<MastodonList> updateList(
@Header("Authorization") String token,
@Path("id") String id,
@Field("title") String title,
@Field("replies_policy") String replies_policy
);
//Delete a conversation
@DELETE("lists/{id}")
Call<Void> deleteList(
@Header("Authorization") String token,
@Path("id") String id
);
//Get accounts in a list
@GET("lists/{id}/accounts")
Call<List<Account>> getAccountsInList(
@Header("Authorization") String token,
@Path("id") String id,
@Query("max_id") String max_id,
@Query("since_id") String since_id,
@Query("limit") int limit
);
//Add account in a list
@FormUrlEncoded
@POST("lists/{id}/accounts")
Call<Void> addAccountsList(
@Header("Authorization") String token,
@Path("id") String id,
@Field("account_ids[]") List<String> account_ids
);
//Delete accounts in a list
@DELETE("lists/{id}/accounts")
Call<Void> deleteAccountsList(
@Header("Authorization") String token,
@Path("id") String id,
@Query("account_ids[]") List<String> account_ids
);
//Get a marker
@GET("markers")
Call<Marker> getMarker(
@Header("Authorization") String token,
@Query("timeline") List<String> timeline
);
//Save marker
@FormUrlEncoded
@POST("markers")
Call<Void> addMarker(
@Header("Authorization") String token,
@Field("home[last_read_id]") String home_last_read_id,
@Field("notifications[last_read_id]") String notifications_last_read_id
);
}

View file

@ -0,0 +1,111 @@
package app.fedilab.android.client.mastodon;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okio.BufferedSink;
public class ProgressRequestBody extends RequestBody {
private static final int SEGMENT_SIZE = 2048;
private final File mFile;
private final String mContentType;
private final ProgressListener mListener;
private final long mTotalToUpload;
private long mLastTotalUploaded;
public ProgressRequestBody(File file, String content_type, long lastTotalUploaded, long totalToUpload, ProgressListener listener) {
mFile = file;
mContentType = content_type;
mListener = listener;
mTotalToUpload = totalToUpload;
mLastTotalUploaded = lastTotalUploaded;
}
@Override
public MediaType contentType() {
return MediaType.parse(mContentType);
}
@Override
public long contentLength() {
return mFile.length();
}
@Override
public void writeTo(@NonNull BufferedSink sink) throws IOException {
byte[] buffer = new byte[SEGMENT_SIZE];
FileInputStream in = new FileInputStream(mFile);
try {
int read;
Handler handler = new Handler(Looper.getMainLooper());
while ((read = in.read(buffer)) != -1) {
// update progress on UI thread
handler.post(new ProgressUpdater(mLastTotalUploaded, mTotalToUpload));
mLastTotalUploaded += read;
sink.write(buffer, 0, read);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
in.close();
}
/* Source source = null;
try {
source = Okio.source(mFile);
long read;
while ((read = source.read(bufferedSink.getBuffer(), SEGMENT_SIZE)) != -1) {
mLastTotalUploaded += read;
bufferedSink.flush();
mListener.onProgressUpdate((int)(100 * mLastTotalUploaded / mTotalToUpload));
}
} finally {
Util.closeQuietly(source);
}*/
}
public interface ProgressListener {
void onProgressUpdate(int percentage);
}
private class ProgressUpdater implements Runnable {
private final long mUploaded;
private final long mTotal;
public ProgressUpdater(long uploaded, long total) {
mUploaded = uploaded;
mTotal = total;
}
@Override
public void run() {
mListener.onProgressUpdate((int) (100 * mUploaded / mTotal));
}
}
}

View file

@ -0,0 +1,101 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.text.Spannable;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
public class Account implements Serializable {
@SerializedName("id")
public String id;
@SerializedName("username")
public String username;
@SerializedName("acct")
public String acct;
@SerializedName("display_name")
public String display_name;
@SerializedName("locked")
public boolean locked;
@SerializedName("bot")
public boolean bot;
@SerializedName("created_at")
public Date created_at;
@SerializedName("note")
public String note;
@SerializedName("url")
public String url;
@SerializedName("avatar")
public String avatar;
@SerializedName("avatar_static")
public String avatar_static;
@SerializedName("header")
public String header;
@SerializedName("header_static")
public String header_static;
@SerializedName("followers_count")
public int followers_count;
@SerializedName("following_count")
public int following_count;
@SerializedName("statuses_count")
public int statuses_count;
@SerializedName("last_status_at")
public Date last_status_at;
@SerializedName("source")
public Source source;
@SerializedName("emojis")
public List<Emoji> emojis;
@SerializedName("fields")
public List<Field> fields;
@SerializedName("suspended")
public boolean suspended;
@SerializedName("discoverable")
public boolean discoverable;
@SerializedName("mute_expires_at")
public Date mute_expires_at;
@SerializedName("moved")
public Account moved;
//Some extra spannable element - They will be filled automatically when fetching the account
public transient Spannable span_display_name;
public transient Spannable span_note;
public transient RelationShip relationShip;
public static class AccountParams implements Serializable {
@SerializedName("discoverable")
public boolean discoverable;
@SerializedName("bot")
public boolean bot;
@SerializedName("display_name")
public String display_name;
@SerializedName("note")
public String note;
@SerializedName("locked")
public boolean locked;
@SerializedName("source")
public Source.SourceParams source;
@SerializedName("fields_attributes")
public LinkedHashMap<Integer, Field.FieldParams> fields;
}
}

View file

@ -0,0 +1,22 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.List;
public class Accounts {
public Pagination pagination = new Pagination();
public List<Account> accounts;
}

View file

@ -0,0 +1,28 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
public class Activity {
@SerializedName("week")
public String week;
@SerializedName("statuses")
public String statuses;
@SerializedName("logins")
public String logins;
@SerializedName("registrations")
public String registrations;
}

View file

@ -0,0 +1,57 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.util.Date;
public class AdminAccount {
@SerializedName("id")
public String id;
@SerializedName("username")
public String username;
@SerializedName("domain")
public String domain;
@SerializedName("created_at")
public Date created_at;
@SerializedName("email")
public String email;
@SerializedName("ip")
public String ip;
@SerializedName("locale")
public String locale;
@SerializedName("invite_request")
public String invite_request;
@SerializedName("role")
public String role;
@SerializedName("confirmed")
public boolean confirmed;
@SerializedName("approved")
public boolean approved;
@SerializedName("disabled")
public boolean disabled;
@SerializedName("silenced")
public boolean silenced;
@SerializedName("suspended")
public boolean suspended;
@SerializedName("account")
public Account account;
@SerializedName("created_by_application_id")
public String created_by_application_id;
@SerializedName("invited_by_account_id")
public String invited_by_account_id;
}

View file

@ -0,0 +1,44 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.util.Date;
import java.util.List;
public class AdminReport {
@SerializedName("id")
public String id;
@SerializedName("action_taken")
public String action_taken;
@SerializedName("comment")
public String comment;
@SerializedName("created_at")
public Date created_at;
@SerializedName("updated_at")
public Date updated_at;
@SerializedName("account")
public Account account;
@SerializedName("target_account")
public Account target_account;
@SerializedName("assigned_account")
public Account assigned_account;
@SerializedName("action_taken_by_account")
public String action_taken_by_account;
@SerializedName("statuses")
public List<Status> statuses;
}

View file

@ -0,0 +1,49 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.util.Date;
import java.util.List;
public class Announcement {
@SerializedName("id")
public String id;
@SerializedName("content")
public String content;
@SerializedName("starts_at")
public Date starts_at;
@SerializedName("ends_at")
public Date ends_at;
@SerializedName("all_day")
public boolean all_day;
@SerializedName("published_at")
public Date published_at;
@SerializedName("updated_at")
public Date updated_at;
@SerializedName("read")
public boolean read;
@SerializedName("mentions")
public List<Mention> mentions;
@SerializedName("statuses")
public List<Status> statuses;
@SerializedName("tags")
public List<Tag> tags;
@SerializedName("emojis")
public List<Emoji> emojis;
@SerializedName("reactions")
public List<Reaction> reactions;
}

View file

@ -0,0 +1,37 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
public class App implements Serializable {
@SerializedName("id")
public String id;
@SerializedName("name")
public String name;
@SerializedName("website")
public String website;
@SerializedName("redirect_uri")
public String redirect_uri;
@SerializedName("client_id")
public String client_id;
@SerializedName("client_secret")
public String client_secret;
@SerializedName("vapid_key")
public String vapid_key;
}

View file

@ -0,0 +1,47 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
public class Attachment implements Serializable {
@SerializedName("id")
public String id;
@SerializedName("type")
public String type;
@SerializedName("url")
public String url;
@SerializedName("preview_url")
public String preview_url;
@SerializedName("remote_url")
public String remote_url;
@SerializedName("text_url")
public String text_url;
@SerializedName("description")
public String description;
@SerializedName("blurhash")
public String blurhash;
@SerializedName("mimeType")
public String mimeType;
@SerializedName("filename")
public String filename;
@SerializedName("size")
public long size;
@SerializedName("local_path")
public String local_path;
}

View file

@ -0,0 +1,50 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
public class Card implements Serializable {
@SerializedName("url")
public String url;
@SerializedName("title")
public String title;
@SerializedName("description")
public String description;
@SerializedName("type")
public String type;
@SerializedName("author_name")
public String author_name;
@SerializedName("author_url")
public String author_url;
@SerializedName("provider_name")
public String provider_name;
@SerializedName("provider_url")
public String provider_url;
@SerializedName("html")
public String html;
@SerializedName("width")
public int width;
@SerializedName("height")
public int height;
@SerializedName("image")
public String image;
@SerializedName("embed_url")
public String embed_url;
@SerializedName("blurhash")
public String blurhash;
}

View file

@ -0,0 +1,27 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class Context {
@SerializedName("ancestors")
public List<Status> ancestors;
@SerializedName("descendants")
public List<Status> descendants;
}

View file

@ -0,0 +1,30 @@
package app.fedilab.android.client.mastodon.entities;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
public class Conversation {
@SerializedName("id")
public String id;
@SerializedName("unread")
public boolean unread;
@SerializedName("accounts")
public List<Account> accounts;
@SerializedName("last_status")
public Status last_status;
}

View file

@ -0,0 +1,22 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import java.util.List;
public class Conversations {
public Pagination pagination = new Pagination();
public List<Conversation> conversations;
}

View file

@ -0,0 +1,32 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
public class Emoji implements Serializable {
@SerializedName("shortcode")
public String shortcode;
@SerializedName("url")
public String url;
@SerializedName("static_url")
public String static_url;
@SerializedName("visible_in_picker")
public boolean visible_in_picker;
@SerializedName("category")
public String category;
}

View file

@ -0,0 +1,217 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.sqlite.Sqlite;
public class EmojiInstance implements Serializable {
private final SQLiteDatabase db;
@SerializedName("instance")
public String instance;
@SerializedName("emojiList")
public List<Emoji> emojiList;
private Context context;
public EmojiInstance() {
db = null;
}
public EmojiInstance(Context context) {
//Creation of the DB with tables
this.context = context;
this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
}
/**
* Serialized a list of Emoji class
*
* @param emojis List of {@link Emoji} to serialize
* @return String serialized emoji list
*/
public static String mastodonEmojiListToStringStorage(List<Emoji> emojis) {
Gson gson = new Gson();
try {
return gson.toJson(emojis);
} catch (Exception e) {
return null;
}
}
/**
* Unserialized a Emoji List
*
* @param serializedEmojiList String serialized account
* @return List of {@link Emoji}
*/
public static List<Emoji> restoreEmojiListFromString(String serializedEmojiList) {
Gson gson = new Gson();
try {
return gson.fromJson(serializedEmojiList, new TypeToken<List<Emoji>>() {
}.getType());
} catch (Exception e) {
return null;
}
}
/**
* Insert or update emoji
*
* @param emojiInstance {@link EmojiInstance}
* @return long - db id
* @throws DBException exception with database
*/
public long insertOrUpdate(EmojiInstance emojiInstance) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
if (emojiInstance == null) {
return -1;
}
boolean exists = emojiInstanceExist(emojiInstance);
long idReturned;
if (exists) {
idReturned = updateEmojiInstance(emojiInstance);
} else {
idReturned = insertEmojiInstance(emojiInstance);
}
return idReturned;
}
/**
* Check if emojis exists in db
*
* @param emojiInstance EmojiInstance {@link EmojiInstance}
* @return boolean - emojiInstance exists
* @throws DBException Exception
*/
public boolean emojiInstanceExist(EmojiInstance emojiInstance) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_EMOJI_INSTANCE
+ " where " + Sqlite.COL_INSTANCE + " = '" + emojiInstance.instance + "'", null);
mCount.moveToFirst();
int count = mCount.getInt(0);
mCount.close();
return (count > 0);
}
/**
* Insert emojis in db
*
* @param emojiInstance {@link EmojiInstance}
* @return long - db id
* @throws DBException exception with database
*/
private long insertEmojiInstance(EmojiInstance emojiInstance) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_INSTANCE, emojiInstance.instance);
values.put(Sqlite.COL_EMOJI_LIST, mastodonEmojiListToStringStorage(emojiInstance.emojiList));
//Inserts token
try {
return db.insertOrThrow(Sqlite.TABLE_EMOJI_INSTANCE, null, values);
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* update emojis in db
*
* @param emojiInstance {@link EmojiInstance}
* @return long - db id
* @throws DBException exception with database
*/
private long updateEmojiInstance(EmojiInstance emojiInstance) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_EMOJI_LIST, mastodonEmojiListToStringStorage(emojiInstance.emojiList));
//Inserts token
try {
return db.update(Sqlite.TABLE_EMOJI_INSTANCE,
values, Sqlite.COL_INSTANCE + " = ?",
new String[]{emojiInstance.instance});
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Returns the emojis for an instance
*
* @param instance String
* @return List<Emoji> - List of {@link Emoji}
*/
public List<Emoji> getEmojiList(String instance) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_EMOJI_INSTANCE, null, Sqlite.COL_INSTANCE + " = '" + instance + "'", null, null, null, null, "1");
return cursorToEmojiList(c);
} catch (Exception e) {
return null;
}
}
/**
* Restore emoji list from db
*
* @param c Cursor
* @return List<Emoji>
*/
private List<Emoji> cursorToEmojiList(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
//Take the first element
c.moveToFirst();
List<Emoji> emojiList = restoreEmojiListFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_EMOJI_LIST)));
c.close();
List<Emoji> filteredEmojis = new ArrayList<>();
if (emojiList != null) {
for (Emoji emoji : emojiList) {
if (emoji.visible_in_picker) {
filteredEmojis.add(emoji);
}
}
}
return filteredEmojis;
}
}

View file

@ -0,0 +1,27 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
public class Error {
@SerializedName("code")
public int code;
@SerializedName("error")
public String error;
@SerializedName("error_description")
public String error_description;
}

View file

@ -0,0 +1,32 @@
package app.fedilab.android.client.mastodon.entities;
import com.google.gson.annotations.SerializedName;
import java.util.Date;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
public class FeaturedTag {
@SerializedName("id")
public String id;
@SerializedName("name")
public String name;
@SerializedName("url")
public String url;
@SerializedName("statuses_count")
public int statuses_count;
@SerializedName("last_status_at")
public Date last_status_at;
}

View file

@ -0,0 +1,41 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.text.Spannable;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.Date;
public class Field implements Serializable {
@SerializedName("name")
public String name;
@SerializedName("value")
public String value;
@SerializedName("verified_at")
public Date verified_at;
//Some extra spannable element - They will be filled automatically when fetching the account
public transient Spannable value_span;
public static class FieldParams implements Serializable {
@SerializedName("name")
public String name;
@SerializedName("value")
public String value;
}
}

View file

@ -0,0 +1,38 @@
package app.fedilab.android.client.mastodon.entities;
import com.google.gson.annotations.SerializedName;
import java.util.Date;
import java.util.List;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
public class Filter {
@SerializedName("id")
public String id;
@SerializedName("phrase")
public String phrase;
@SerializedName("context")
public List<String> context;
@SerializedName("whole_word")
public boolean whole_word;
@SerializedName("expires_at")
public Date expires_at;
@SerializedName("expires_at_sent")
public long expires_at_sent;
@SerializedName("irreversible")
public boolean irreversible;
}

View file

@ -0,0 +1,26 @@
package app.fedilab.android.client.mastodon.entities;
import com.google.gson.annotations.SerializedName;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
public class History {
@SerializedName("day")
public String day;
@SerializedName("uses")
public String uses;
@SerializedName("accounts")
public String accounts;
}

View file

@ -0,0 +1,32 @@
package app.fedilab.android.client.mastodon.entities;
import com.google.gson.annotations.SerializedName;
import java.util.Date;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
public class IdentityProof {
@SerializedName("provider")
public String provider;
@SerializedName("provider_username")
public String provider_username;
@SerializedName("updated_at")
public Date updated_at;
@SerializedName("proof_url")
public String proof_url;
@SerializedName("profile_url")
public String profile_url;
}

View file

@ -0,0 +1,178 @@
package app.fedilab.android.client.mastodon.entities;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
public class Instance implements Serializable {
@SerializedName("uri")
public String uri;
@SerializedName("title")
public String title;
@SerializedName("short_description")
public String short_description;
@SerializedName("description")
public String description;
@SerializedName("email")
public String email;
@SerializedName("version")
public String version;
@SerializedName("languages")
public List<String> languages;
@SerializedName("registrations")
public boolean registrations;
@SerializedName("rules")
public List<Rule> rules;
@SerializedName("approval_required")
public boolean approval_required;
@SerializedName("invites_enabled")
public boolean invites_enabled;
@SerializedName("stats")
public Stats stats;
@SerializedName("urls")
public Urls urls;
@SerializedName("thumbnail")
public String thumbnail;
@SerializedName("contact_account")
public Account contact_account;
@SerializedName("configuration")
public Configuration configuration;
public List<String> getMimeTypeAudio() {
List<String> mimeTypes = new ArrayList<>();
if (configuration == null || configuration.media_attachments == null) {
return mimeTypes;
}
for (String mimeType : configuration.media_attachments.supported_mime_types) {
if (mimeType.startsWith("audio")) {
mimeTypes.add(mimeType);
}
}
return mimeTypes;
}
public List<String> getMimeTypeVideo() {
List<String> mimeTypes = new ArrayList<>();
if (configuration == null || configuration.media_attachments == null) {
return mimeTypes;
}
for (String mimeType : configuration.media_attachments.supported_mime_types) {
if (mimeType.startsWith("video")) {
mimeTypes.add(mimeType);
}
}
return mimeTypes;
}
public List<String> getMimeTypeImage() {
List<String> mimeTypes = new ArrayList<>();
if (configuration == null || configuration.media_attachments == null) {
return mimeTypes;
}
for (String mimeType : configuration.media_attachments.supported_mime_types) {
if (mimeType.startsWith("image")) {
mimeTypes.add(mimeType);
}
}
return mimeTypes;
}
public List<String> getMimeTypeOther() {
List<String> mimeTypes = new ArrayList<>();
if (configuration == null || configuration.media_attachments == null) {
return mimeTypes;
}
for (String mimeType : configuration.media_attachments.supported_mime_types) {
if (!mimeType.startsWith("image") && !mimeType.startsWith("video") && !mimeType.startsWith("audio")) {
mimeTypes.add(mimeType);
}
}
return mimeTypes;
}
public static class Configuration implements Serializable {
@SerializedName("statuses")
public StatusesConf statusesConf;
@SerializedName("polls")
public PollsConf pollsConf;
@SerializedName("media_attachments")
public MediaConf media_attachments;
}
public static class StatusesConf implements Serializable {
@SerializedName("max_characters")
public int max_characters = 500;
@SerializedName("max_media_attachments")
public int max_media_attachments = 4;
@SerializedName("characters_reserved_per_url")
public int characters_reserved_per_url;
}
public static class MediaConf implements Serializable {
@SerializedName("supported_mime_types")
public List<String> supported_mime_types;
@SerializedName("image_size_limit")
public int image_size_limit;
@SerializedName("image_matrix_limit")
public int image_matrix_limit;
@SerializedName("video_size_limit")
public int video_size_limit;
@SerializedName("video_frame_rate_limit")
public int video_frame_rate_limit;
@SerializedName("video_matrix_limit")
public int video_matrix_limit;
}
public static class PollsConf implements Serializable {
@SerializedName("min_expiration")
public int min_expiration;
@SerializedName("max_options")
public int max_options = 4;
@SerializedName("max_option_chars")
public int max_option_chars = 25;
@SerializedName("max_expiration")
public int max_expiration;
}
public static class Stats implements Serializable {
@SerializedName("user_count")
public int user_count;
@SerializedName("status_count")
public int status_count;
@SerializedName("domain_count")
public int domain_count;
}
public static class Urls implements Serializable {
@SerializedName("streaming_api")
public String streaming_api;
}
public static class Rule implements Serializable {
@SerializedName("id")
public String id;
@SerializedName("text")
public String text;
public transient boolean isChecked = false;
}
}

View file

@ -0,0 +1,204 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.sqlite.Sqlite;
public class InstanceInfo implements Serializable {
private final SQLiteDatabase db;
@SerializedName("instance")
public String instance;
@SerializedName("info")
public Instance info;
public InstanceInfo() {
db = null;
}
public InstanceInfo(Context context) {
//Creation of the DB with tables
this.db = Sqlite.getInstance(context.getApplicationContext(), Sqlite.DB_NAME, null, Sqlite.DB_VERSION).open();
}
/**
* Serialized a list of Emoji class
*
* @param instance {@link Instance} to serialize
* @return String serialized instance
*/
public static String instanceInfoToStringStorage(Instance instance) {
Gson gson = new Gson();
try {
return gson.toJson(instance);
} catch (Exception e) {
return null;
}
}
/**
* Unserialized an instance
*
* @param serializedInstanceInfo String serialized instance
* @return {@link Instance}
*/
public static Instance restoreInstanceInfoFromString(String serializedInstanceInfo) {
Gson gson = new Gson();
try {
return gson.fromJson(serializedInstanceInfo, Instance.class);
} catch (Exception e) {
return null;
}
}
/**
* Insert or update instance
*
* @param instanceInfo {@link InstanceInfo}
* @return long - db id
* @throws DBException exception with database
*/
public long insertOrUpdate(InstanceInfo instanceInfo) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
if (instanceInfo == null) {
return -1;
}
boolean exists = instanceInfoExist(instanceInfo);
long idReturned;
if (exists) {
idReturned = updateInstanceInfo(instanceInfo);
} else {
idReturned = insertInstanceInfo(instanceInfo);
}
return idReturned;
}
/**
* Check if instanceInfo exists in db
*
* @param instanceInfo InstanceInfo {@link InstanceInfo}
* @return boolean - instanceInfo exists
* @throws DBException Exception
*/
public boolean instanceInfoExist(InstanceInfo instanceInfo) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
Cursor mCount = db.rawQuery("select count(*) from " + Sqlite.TABLE_INSTANCE_INFO
+ " where " + Sqlite.COL_INSTANCE + " = '" + instanceInfo.instance + "'", null);
mCount.moveToFirst();
int count = mCount.getInt(0);
mCount.close();
return (count > 0);
}
/**
* Insert instanceInfo in db
*
* @param instanceInfo {@link InstanceInfo}
* @return long - db id
* @throws DBException exception with database
*/
private long insertInstanceInfo(InstanceInfo instanceInfo) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_INSTANCE, instanceInfo.instance);
values.put(Sqlite.COL_INFO, instanceInfoToStringStorage(instanceInfo.info));
//Inserts instance
try {
return db.insertOrThrow(Sqlite.TABLE_INSTANCE_INFO, null, values);
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* update instanceInfo in db
*
* @param instanceInfo {@link InstanceInfo}
* @return long - db id
* @throws DBException exception with database
*/
private long updateInstanceInfo(InstanceInfo instanceInfo) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
ContentValues values = new ContentValues();
values.put(Sqlite.COL_INFO, instanceInfoToStringStorage(instanceInfo.info));
//Inserts token
try {
return db.update(Sqlite.TABLE_INSTANCE_INFO,
values, Sqlite.COL_INSTANCE + " = ?",
new String[]{instanceInfo.instance});
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
/**
* Returns the info for an instance
*
* @param instance String
* @return InstanceInfo - {@link InstanceInfo}
*/
public Instance getInstanceInfo(String instance) throws DBException {
if (db == null) {
throw new DBException("db is null. Wrong initialization.");
}
try {
Cursor c = db.query(Sqlite.TABLE_INSTANCE_INFO, null, Sqlite.COL_INSTANCE + " = '" + instance + "'", null, null, null, null, "1");
return cursorToInstanceInfo(c);
} catch (Exception e) {
return null;
}
}
/**
* Restore instanceInfo from db
*
* @param c Cursor
* @return Instance
*/
private Instance cursorToInstanceInfo(Cursor c) {
//No element found
if (c.getCount() == 0) {
c.close();
return null;
}
//Take the first element
c.moveToFirst();
Instance instance = restoreInstanceInfoFromString(c.getString(c.getColumnIndexOrThrow(Sqlite.COL_INFO)));
c.close();
return instance;
}
}

View file

@ -0,0 +1,44 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class JoinMastodonInstance {
@SerializedName("domain")
public String domain;
@SerializedName("version")
public String version;
@SerializedName("description")
public String description;
@SerializedName("languages")
public List<String> languages;
@SerializedName("categories")
public List<String> categories;
@SerializedName("proxied_thumbnail")
public String proxied_thumbnail;
@SerializedName("total_users")
public int total_users;
@SerializedName("last_week_users")
public int last_week_users;
@SerializedName("approval_required")
public boolean approval_required;
@SerializedName("language")
public String language;
@SerializedName("general")
public String general;
}

View file

@ -0,0 +1,37 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.util.Date;
public class Marker {
@SerializedName("home")
public MarkerContent home;
@SerializedName("notifications")
public MarkerContent notifications;
public static class MarkerContent {
@SerializedName("last_read_id")
public String last_read_id;
@SerializedName("version")
public int version;
@SerializedName("updated_at")
public Date updated_at;
}
}

View file

@ -0,0 +1,29 @@
package app.fedilab.android.client.mastodon.entities;
/* Copyright 2021 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;
public class MastodonList implements Serializable {
@SerializedName("id")
public String id;
@SerializedName("title")
public String title;
@SerializedName("replies_policy")
public String replies_policy;
}

Some files were not shown because too many files have changed in this diff Show more