├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .idea ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── migrations.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── channel.css │ ├── channel.html │ ├── channel.js │ └── channel.lass │ ├── java │ └── org │ │ └── shirakumo │ │ └── ocelot │ │ ├── About.java │ │ ├── Channel.java │ │ ├── Chat.java │ │ ├── Command.java │ │ ├── DynamicListPreference.java │ │ ├── EmoteList.java │ │ ├── FirstTimeSetup.java │ │ ├── PayloadView.java │ │ ├── Service.java │ │ ├── Settings.java │ │ ├── Toolkit.java │ │ └── UpdateHandler.java │ └── res │ ├── drawable │ ├── channel_button.xml │ ├── ic_add_circle_black_24dp.xml │ ├── ic_attach_file_black_24dp.xml │ ├── ic_close_black_24dp.xml │ ├── ic_exit_to_app_black_24dp.xml │ ├── ic_group_add_black_24dp.xml │ ├── ic_info_outline_black_24dp.xml │ ├── ic_input_black_24dp.xml │ ├── ic_insert_emoticon_black_24dp.xml │ ├── ic_looks_black_24dp.xml │ ├── ic_menu.xml │ ├── ic_notifications_black_24dp.xml │ ├── ic_ocelot.xml │ ├── ic_people_outline_black_24dp.xml │ ├── ic_person_add_black_24dp.xml │ ├── ic_person_outline_black_24dp.xml │ ├── ic_remove_circle_black_24dp.xml │ ├── ic_settings_black_24dp.xml │ ├── ic_settings_ethernet_black_24dp.xml │ └── white_circle.xml │ ├── layout │ ├── activity_chat.xml │ ├── activity_first_time_setup.xml │ ├── channel_button.xml │ ├── dynamic_list_preference.xml │ ├── dynamic_list_preference_entry.xml │ ├── fragment_about.xml │ ├── fragment_channel.xml │ └── fragment_emote_list.xml │ ├── menu │ └── drawer.xml │ ├── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ └── strings.xml │ └── xml │ ├── provider_paths.xml │ ├── settings_connection.xml │ ├── settings_headers.xml │ ├── settings_looks.xml │ └── settings_notification.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jLichat ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── consumer-rules.pro ├── pom.xml ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── org │ └── shirakumo │ └── lichat │ ├── Base64.java │ ├── CL.java │ ├── Client.java │ ├── Condition.java │ ├── Handler.java │ ├── HandlerAdapter.java │ ├── InputStream.java │ ├── OutputStream.java │ ├── Package.java │ ├── Payload.java │ ├── Printer.java │ ├── Reader.java │ ├── StandardObject.java │ ├── Symbol.java │ ├── Test.java │ ├── conditions │ ├── AlreadyConnected.java │ ├── EncodingUnsupported.java │ ├── EndOfStream.java │ ├── InstantiationFailed.java │ ├── InvalidClassDefinition.java │ ├── InvalidUpdateReceived.java │ ├── MalformedWireObject.java │ ├── MissingArgument.java │ ├── NoSuchClass.java │ ├── NotConnected.java │ ├── PackageAlreadyExists.java │ ├── PingTimeout.java │ ├── SlotMissing.java │ ├── SlotValueFailed.java │ ├── UnmatchedCloseParen.java │ ├── UnprintableObject.java │ └── WriteError.java │ └── updates │ ├── AlreadyInChannel.java │ ├── Backfill.java │ ├── BadContentType.java │ ├── BadName.java │ ├── Ban.java │ ├── Capabilities.java │ ├── ChannelInfo.java │ ├── ChannelUpdate.java │ ├── ChannelnameTaken.java │ ├── Channels.java │ ├── Connect.java │ ├── ConnectionLost.java │ ├── ConnectionUnstable.java │ ├── Create.java │ ├── Data.java │ ├── Deny.java │ ├── Destroy.java │ ├── Disconnect.java │ ├── Edit.java │ ├── Emote.java │ ├── Emotes.java │ ├── Failure.java │ ├── Grant.java │ ├── IncompatibleVersion.java │ ├── InsufficientPermissions.java │ ├── InvalidPassword.java │ ├── InvalidPermissions.java │ ├── InvalidUpdate.java │ ├── IpBan.java │ ├── IpUnban.java │ ├── Join.java │ ├── Kick.java │ ├── Kill.java │ ├── Leave.java │ ├── MalformedUpdate.java │ ├── Message.java │ ├── NoSuchChannel.java │ ├── NoSuchChannelInfo.java │ ├── NoSuchProfile.java │ ├── NoSuchUser.java │ ├── NoSuchUserInfo.java │ ├── NotInChannel.java │ ├── Pause.java │ ├── Permissions.java │ ├── Ping.java │ ├── Pong.java │ ├── Pull.java │ ├── Quiet.java │ ├── Register.java │ ├── ServerInfo.java │ ├── SetChannelInfo.java │ ├── SetUserInfo.java │ ├── TargetUpdate.java │ ├── TextUpdate.java │ ├── TooManyConnections.java │ ├── TooManyUpdates.java │ ├── Unban.java │ ├── Unquiet.java │ ├── Update.java │ ├── UpdateFailure.java │ ├── UpdateTooLong.java │ ├── UserInfo.java │ ├── UsernameMismatch.java │ ├── UsernameTaken.java │ └── Users.java ├── metadata └── en-US │ ├── changelogs │ └── 3.txt │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ └── 3.png │ ├── short_description.txt │ └── title.txt ├── settings.gradle └── shirakumo.jks.gpg /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build APK 2 | on: [create, workflow_dispatch] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout the code 9 | uses: actions/checkout@v2 10 | - name: Setup Java 11 | uses: actions/setup-java@v4 12 | with: 13 | distribution: 'temurin' 14 | java-version: '21' 15 | - name: Prepare keys 16 | run: | 17 | gpg --quiet --batch --yes --decrypt --passphrase "$RELEASE_PASSWORD" --output $HOME/shirakumo.jks shirakumo.jks.gpg 18 | mkdir -p $HOME/.gradle 19 | echo "RELEASE_STORE_FILE=$HOME/shirakumo.jks" >> $HOME/.gradle/gradle.properties 20 | echo "RELEASE_STORE_PASSWORD=$RELEASE_PASSWORD" >> $HOME/.gradle/gradle.properties 21 | echo "RELEASE_KEY_ALIAS=shirakumo" >> $HOME/.gradle/gradle.properties 22 | echo "RELEASE_KEY_PASSWORD=$RELEASE_PASSWORD" >> $HOME/.gradle/gradle.properties 23 | env: 24 | RELEASE_PASSWORD: ${{ secrets.RELEASE_PASSWORD }} 25 | - name: Build the app 26 | run: ./gradlew build 27 | - name: Assemble 28 | run: ./gradlew assembleRelease 29 | - name: Upload artifact 30 | uses: actions/upload-artifact@v1 31 | with: 32 | name: app 33 | path: app/build/outputs/apk/release/app-release.apk 34 | - name: Create release 35 | id: create_release 36 | uses: actions/create-release@v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | tag_name: rel-${{ github.ref }} 41 | release_name: Release ${{ github.ref }} 42 | draft: false 43 | prerelease: false 44 | - name: Upload release 45 | uses: actions/upload-release-asset@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ steps.create_release.outputs.upload_url }} 50 | asset_path: app/build/outputs/apk/release/app-release.apk 51 | asset_name: ocelot.apk 52 | asset_content_type: application/vnd.android.package-archive 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | jLichat/build -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 28 | 49 | 50 | 51 | 52 | 53 | 54 | 56 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Yukari Hafner 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ocelot GitHub Get it on F-Droid 2 | ====== 3 | 4 | A chat client for the Lichat protocol. 5 | 6 | This supports the full Lichat 2.0 protocol, including most extensions. 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 34 5 | defaultConfig { 6 | applicationId "org.shirakumo.ocelot" 7 | minSdkVersion 21 8 | targetSdkVersion 34 9 | versionCode 6 10 | versionName '0.6' 11 | } 12 | signingConfigs{ 13 | release { 14 | storeFile file(RELEASE_STORE_FILE) 15 | storePassword RELEASE_STORE_PASSWORD 16 | keyAlias RELEASE_KEY_ALIAS 17 | keyPassword RELEASE_KEY_PASSWORD 18 | v1SigningEnabled true 19 | v2SigningEnabled true 20 | } 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | signingConfig signingConfigs.release 27 | } 28 | } 29 | productFlavors { 30 | } 31 | compileOptions { 32 | targetCompatibility 1.8 33 | sourceCompatibility 1.8 34 | } 35 | buildFeatures { 36 | buildConfig true 37 | } 38 | namespace 'org.shirakumo.ocelot' 39 | lint { 40 | abortOnError false 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation project(':jLichat') 46 | implementation 'androidx.preference:preference:1.2.1' 47 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 48 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 49 | implementation 'com.google.android.material:material:1.11.0' 50 | implementation 'com.rarepebble:colorpicker:2.2.0' 51 | } 52 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 48 | 49 | 54 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/assets/channel.css: -------------------------------------------------------------------------------- 1 | #channel{font-family:monospace;}#channel .update{margin-top:0.2em;}#channel .update >*{display:flex;align-items:flex-start;}#channel .update .content .clock{color:gray;}#channel .update .clock{text-align:center;margin-right:0.5em;}#channel .update .from{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin-left:0.5em;min-width:100px;}#channel .update .text{word-wrap:break-word;white-space:pre-wrap;min-width:0px;flex-grow:1;}#channel .update .text a{text-decoration:none;}#channel .update .text .payload{max-width:100%;max-height:50vh;} -------------------------------------------------------------------------------- /app/src/main/assets/channel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/assets/channel.js: -------------------------------------------------------------------------------- 1 | var channel = document.getElementById("channel"); 2 | 3 | var constructElement = function(options){ 4 | var el = document.createElement(options.tag); 5 | el.setAttribute("class", (options.classes||[]).join(" ")); 6 | if(options.text) el.innerText = options.text; 7 | if(options.html) el.innerHTML = options.html; 8 | for(var attr in (options.attributes||{})){ 9 | el.setAttribute(attr, options.attributes[attr]); 10 | } 11 | for(var i in (options.elements||[])){ 12 | el.appendChild(constructElement(options.elements[i])); 13 | } 14 | return el; 15 | }; 16 | 17 | var objectColor = function(object){ 18 | var str = object.toString(); 19 | var hash = 0; 20 | for(var i=0; i>8)-1; 25 | var g = 16*(1+(encoded&0x0F0)>>4)-1; 26 | var b = 16*(1+(encoded&0x00F)>>0)-1; 27 | 28 | return "rgb("+Math.min(200, Math.max(50, r)) 29 | +","+Math.min(180, Math.max(80, g)) 30 | +","+Math.min(180, Math.max(80, b))+")"; 31 | }; 32 | 33 | var formatTime = function(time){ 34 | var date = new Date(time*1000); 35 | var pd = function(a){return (a<10)?"0"+a:""+a;} 36 | return pd(date.getHours())+":"+pd(date.getMinutes())+":"+pd(date.getSeconds()); 37 | }; 38 | 39 | var reloadCSS = function(){ 40 | var links = document.getElementsByTagName("link"); 41 | for(var i in links){ 42 | var link = links[i]; 43 | if(link.rel === "stylesheet"){ 44 | var h = link.href.replace(/(&|\?)forceReload=\d /,""); 45 | link.href=h+(h.indexOf('?')>=0?'&':'?')+"forceReload="+(new Date().valueOf()); 46 | } 47 | } 48 | }; 49 | 50 | var clear = function(){ 51 | while(channel.firstChild){ 52 | channel.removeChild(channel.firstChild); 53 | } 54 | }; 55 | 56 | var matchesFrom = function(node, from){ 57 | return node.children[0].children[1].innerText === from; 58 | }; 59 | 60 | var isAtBottom = function(){ 61 | return (window.innerHeight + window.scrollY) >= document.body.offsetHeight; 62 | }; 63 | 64 | var lastInserted = null; 65 | var addScrollHandler = function(element){ 66 | if(!isAtBottom()) return element; 67 | lastInserted = element; 68 | var elements = element.querySelectorAll("img,audio,video"); 69 | for(var i=0; i* :display flex 6 | :align-items flex-start) 7 | ((.content .clock) :color gray) 8 | (.clock :text-align center 9 | :margin-right 0.5em) 10 | (.from :overflow hidden 11 | :white-space nowrap 12 | :text-overflow ellipsis 13 | :margin-left 0.5em 14 | :min-width 100px) 15 | (.text :word-wrap break-word 16 | :white-space pre-wrap 17 | :min-width 0px 18 | :flex-grow 1 19 | (a :text-decoration none) 20 | (.payload 21 | :max-width 100% 22 | :max-height 50vh)))) 23 | -------------------------------------------------------------------------------- /app/src/main/java/org/shirakumo/ocelot/About.java: -------------------------------------------------------------------------------- 1 | package org.shirakumo.ocelot; 2 | 3 | import android.app.DialogFragment; 4 | import android.os.Bundle; 5 | import android.text.Html; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.TextView; 10 | 11 | public class About extends DialogFragment { 12 | public About() { 13 | // Required empty public constructor 14 | } 15 | 16 | public static About newInstance() { 17 | return new About(); 18 | } 19 | 20 | public void onResume(){ 21 | super.onResume(); 22 | setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_AppCompat_Light_Dialog_Alert); 23 | } 24 | 25 | @Override 26 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 27 | Bundle savedInstanceState) { 28 | View view = inflater.inflate(R.layout.fragment_about, container, false); 29 | ((TextView)view.findViewById(R.id.about_version)).setText(BuildConfig.VERSION_NAME); 30 | 31 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 32 | ((TextView) view.findViewById(R.id.about_text)).setText(Html.fromHtml(getString(R.string.app_description), Html.FROM_HTML_MODE_COMPACT)); 33 | }else{ 34 | ((TextView) view.findViewById(R.id.about_text)).setText(Html.fromHtml(getString(R.string.app_description))); 35 | } 36 | return view; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/org/shirakumo/ocelot/Command.java: -------------------------------------------------------------------------------- 1 | package org.shirakumo.ocelot; 2 | 3 | @FunctionalInterface 4 | public interface Command { 5 | public void execute(Channel channel, String[] args); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/org/shirakumo/ocelot/DynamicListPreference.java: -------------------------------------------------------------------------------- 1 | package org.shirakumo.ocelot; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.content.res.TypedArray; 6 | import android.preference.DialogPreference; 7 | import android.util.AttributeSet; 8 | import android.view.KeyEvent; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.inputmethod.EditorInfo; 12 | import android.widget.Button; 13 | import android.widget.EditText; 14 | import android.widget.ImageButton; 15 | import android.widget.LinearLayout; 16 | import android.widget.TextView; 17 | 18 | import java.util.Arrays; 19 | import java.util.Set; 20 | import java.util.TreeSet; 21 | 22 | public class DynamicListPreference extends DialogPreference { 23 | private Set entries = new TreeSet<>(); 24 | 25 | public DynamicListPreference(Context context, AttributeSet attrs){ 26 | this(context, attrs, 0); 27 | } 28 | 29 | public DynamicListPreference(Context context, AttributeSet attrs, int defStyleAttr){ 30 | super(context, attrs, defStyleAttr); 31 | 32 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DynamicListPreference); 33 | int entriesId = ta.getResourceId(R.styleable.DynamicListPreference_entries, -1); 34 | if(entriesId != -1){ 35 | entries.addAll(Arrays.asList(context.getResources().getStringArray(entriesId))); 36 | } 37 | ta.recycle(); 38 | 39 | setDialogLayoutResource(R.layout.dynamic_list_preference); 40 | } 41 | 42 | private void addEntry(LinearLayout view, String entry){ 43 | LayoutInflater li = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 44 | View entryView = li.inflate(R.layout.dynamic_list_preference_entry, null); 45 | 46 | ((TextView)entryView.findViewById(R.id.entry)).setText(entry); 47 | entryView.findViewById(R.id.remove_entry).setOnClickListener((vw)->{ 48 | entries.remove(entry); 49 | view.removeView(entryView); 50 | }); 51 | view.addView(entryView); 52 | } 53 | 54 | @Override 55 | protected void onBindDialogView(View view) { 56 | super.onBindDialogView(view); 57 | ImageButton add_entry = view.findViewById(R.id.add_entry); 58 | EditText entry_name = view.findViewById(R.id.new_entry); 59 | LinearLayout entries = view.findViewById(R.id.entries); 60 | 61 | entry_name.setOnEditorActionListener((TextView vw, int actionId, KeyEvent event)->{ 62 | if (actionId == EditorInfo.IME_ACTION_DONE || 63 | (event != null && !event.isShiftPressed() && event.getKeyCode() == KeyEvent.KEYCODE_ENTER)){ 64 | add_entry.callOnClick(); 65 | return true; 66 | } 67 | return false; 68 | }); 69 | 70 | add_entry.setOnClickListener((vw)->{ 71 | String channel = entry_name.getText().toString(); 72 | if(!channel.isEmpty()){ 73 | entry_name.setText(""); 74 | if(this.entries.add(channel)) 75 | addEntry(entries, channel); 76 | } 77 | }); 78 | 79 | for(String entry : this.entries){ 80 | addEntry(entries, entry); 81 | } 82 | } 83 | 84 | @Override 85 | protected void onDialogClosed(boolean positiveResult) { 86 | SharedPreferences.Editor edit = getSharedPreferences().edit(); 87 | edit.putStringSet(getKey(), entries); 88 | edit.apply(); 89 | } 90 | 91 | @Override 92 | protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { 93 | entries.clear(); 94 | if (restorePersistedValue) { 95 | entries.addAll(getSharedPreferences().getStringSet(getKey(), new TreeSet<>())); 96 | } else { 97 | entries.addAll((Set)defaultValue); 98 | } 99 | } 100 | 101 | @Override 102 | protected Object onGetDefaultValue(TypedArray a, int index) { 103 | return a.getTextArray(index); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/org/shirakumo/ocelot/EmoteList.java: -------------------------------------------------------------------------------- 1 | package org.shirakumo.ocelot; 2 | 3 | import android.app.Activity; 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.os.Bundle; 7 | import android.app.DialogFragment; 8 | import android.util.DisplayMetrics; 9 | import android.util.Log; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.BaseAdapter; 14 | import android.widget.GridView; 15 | 16 | import org.shirakumo.lichat.Payload; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Arrays; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | public class EmoteList extends DialogFragment { 24 | 25 | EmoteListListener listener; 26 | View view; 27 | List images = new ArrayList<>(); 28 | 29 | public EmoteList() { 30 | // Required empty public constructor 31 | } 32 | 33 | public static EmoteList newInstance() { 34 | return new EmoteList(); 35 | } 36 | 37 | public void onResume(){ 38 | super.onResume(); 39 | setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_AppCompat_Light_Dialog_Alert); 40 | } 41 | 42 | @Override 43 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 44 | Bundle savedInstanceState) { 45 | view = inflater.inflate(R.layout.fragment_emote_list, container, false); 46 | GridView grid = view.findViewById(R.id.emotes); 47 | grid.setAdapter(new GridViewAdapter()); 48 | 49 | if(listener != null) showEmotes(listener.getEmotes()); 50 | return view; 51 | } 52 | 53 | public void showEmotes(Map emotes){ 54 | String[] names = new String[emotes.size()]; 55 | names = emotes.keySet().toArray(names); 56 | Arrays.sort(names); 57 | for(String name : names){ 58 | // FIXME: cache views 59 | PayloadView emote = new PayloadView(view.getContext(), emotes.get(name)); 60 | emote.setOnClickListener((v)->{ 61 | listener.onEmoteChosen(name); 62 | dismiss(); 63 | }); 64 | images.add(emote); 65 | } 66 | } 67 | 68 | @Override 69 | public void onAttach(Activity context) { 70 | super.onAttach(context); 71 | try { 72 | listener = (EmoteListListener) context; 73 | if(view != null) showEmotes(listener.getEmotes()); 74 | } catch (ClassCastException e) { 75 | throw new ClassCastException(context.toString()+" must implement EmoteListListener"); 76 | } 77 | } 78 | 79 | @Override 80 | public void onAttach(Context context) { 81 | super.onAttach(context); 82 | try { 83 | listener = (EmoteListListener) context; 84 | if(view != null) showEmotes(listener.getEmotes()); 85 | } catch (ClassCastException e) { 86 | throw new ClassCastException(context.toString()+" must implement EmoteListListener"); 87 | } 88 | } 89 | 90 | @Override 91 | public void onDetach() { 92 | super.onDetach(); 93 | listener = null; 94 | } 95 | 96 | public interface EmoteListListener{ 97 | public void onEmoteChosen(String emote); 98 | public Map getEmotes(); 99 | } 100 | 101 | private class GridViewAdapter extends BaseAdapter{ 102 | @Override 103 | public int getCount() { 104 | return images.size(); 105 | } 106 | 107 | @Override 108 | public Object getItem(int position) { 109 | return null; 110 | } 111 | 112 | @Override 113 | public long getItemId(int position) { 114 | return 0; 115 | } 116 | 117 | @Override 118 | public View getView(int position, View convertView, ViewGroup parent) { 119 | return images.get(position); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/org/shirakumo/ocelot/FirstTimeSetup.java: -------------------------------------------------------------------------------- 1 | package org.shirakumo.ocelot; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.preference.PreferenceManager; 7 | import android.view.View; 8 | import android.widget.CheckBox; 9 | import android.widget.EditText; 10 | import android.widget.Switch; 11 | 12 | public class FirstTimeSetup extends Activity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_first_time_setup); 18 | EditText username = ((EditText)findViewById(R.id.setup_username)); 19 | EditText password = ((EditText)findViewById(R.id.setup_password)); 20 | EditText hostname = ((EditText)findViewById(R.id.setup_hostname)); 21 | EditText port = ((EditText)findViewById(R.id.setup_port)); 22 | CheckBox registered = ((CheckBox)findViewById(R.id.setup_registered)); 23 | 24 | registered.setOnCheckedChangeListener((vw, ticked)->{ 25 | password.setVisibility(ticked? View.VISIBLE : View.GONE); 26 | }); 27 | 28 | findViewById(R.id.setup_complete).setOnClickListener((vw)->{ 29 | boolean error = false; 30 | 31 | if(32 < username.getText().length()){ 32 | username.setError("The username must be less than 32 characters long."); 33 | error = true; 34 | } 35 | 36 | if(password.getText().length() < 6 && registered.isChecked()){ 37 | password.setError("Registered accounts need a password with 6 to 32 characters."); 38 | error = true; 39 | } 40 | 41 | if(65535 < Integer.parseInt(port.getText().toString())){ 42 | port.setError("There are not ports above 65535."); 43 | error = true; 44 | } 45 | 46 | if(!error) { 47 | PreferenceManager.getDefaultSharedPreferences(this).edit() 48 | .putBoolean("setup", true) 49 | .putString("username", username.getText().toString()) 50 | .putString("password", password.getText().toString()) 51 | .putString("hostname", hostname.getText().toString()) 52 | .putString("port", port.getText().toString()) 53 | .putBoolean("autoconnect", ((Switch) findViewById(R.id.setup_autoconnect)).isChecked()) 54 | .apply(); 55 | 56 | Intent intent = new Intent(); 57 | intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS"); 58 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 59 | intent.putExtra("app_package", getBaseContext().getPackageName()); 60 | intent.putExtra("app_uid", getBaseContext().getApplicationInfo().uid); 61 | intent.putExtra("android.provider.extra.APP_PACKAGE", getBaseContext().getPackageName()); 62 | getBaseContext().startActivity(intent); 63 | 64 | finish(); 65 | } 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/org/shirakumo/ocelot/PayloadView.java: -------------------------------------------------------------------------------- 1 | package org.shirakumo.ocelot; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.graphics.Canvas; 7 | import android.graphics.Movie; 8 | import android.view.View; 9 | 10 | import org.shirakumo.lichat.Payload; 11 | 12 | public class PayloadView extends View { 13 | Bitmap image; 14 | Movie movie; 15 | long start = 0; 16 | 17 | public PayloadView(Context context, Payload payload){ 18 | super(context); 19 | if(payload.contentType.equals("image/gif")){ 20 | movie = Movie.decodeByteArray(payload.data, 0, payload.data.length); 21 | }else{ 22 | image = BitmapFactory.decodeByteArray(payload.data, 0, payload.data.length, null); 23 | } 24 | } 25 | 26 | @Override 27 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 28 | if(movie != null) 29 | setMeasuredDimension(movie.width()*2, movie.height()*2); 30 | else if(image != null) 31 | setMeasuredDimension(image.getWidth()*2, image.getHeight()*2); 32 | else 33 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 34 | } 35 | 36 | @Override 37 | protected void onDraw(Canvas canvas) { 38 | drawContent(canvas); 39 | } 40 | 41 | private void drawContent(Canvas canvas){ 42 | canvas.scale(2f, 2f); 43 | if (movie != null) { 44 | long now = android.os.SystemClock.uptimeMillis(); 45 | if (start == 0) { start = now; } 46 | 47 | int dur = movie.duration(); 48 | if (dur <= 0) { 49 | dur = 1000; 50 | } 51 | 52 | movie.setTime((int)((now - start) % dur)); 53 | 54 | movie.draw(canvas, 0, 0); 55 | invalidate(); 56 | }else if(image != null){ 57 | canvas.drawBitmap(image, 0, 0, null); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/org/shirakumo/ocelot/Settings.java: -------------------------------------------------------------------------------- 1 | package org.shirakumo.ocelot; 2 | 3 | import android.content.Intent; 4 | import android.content.SharedPreferences; 5 | import android.os.Build; 6 | import android.os.Bundle; 7 | import android.preference.Preference; 8 | import android.preference.PreferenceActivity; 9 | import android.preference.PreferenceFragment; 10 | import android.preference.PreferenceManager; 11 | 12 | import java.io.File; 13 | import java.util.List; 14 | 15 | public class Settings extends PreferenceActivity { 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | } 21 | 22 | @Override 23 | public void onBuildHeaders(List
target) { 24 | loadHeadersFromResource(R.xml.settings_headers, target); 25 | } 26 | 27 | @Override 28 | protected boolean isValidFragment (String fragmentName) { 29 | return true; 30 | } 31 | 32 | 33 | 34 | private abstract static class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener{ 35 | @Override 36 | public void onResume() { 37 | super.onResume(); 38 | getPreferenceScreen() 39 | .getSharedPreferences() 40 | .registerOnSharedPreferenceChangeListener(this); 41 | } 42 | 43 | @Override 44 | public void onPause() { 45 | super.onPause(); 46 | getPreferenceScreen() 47 | .getSharedPreferences() 48 | .unregisterOnSharedPreferenceChangeListener(this); 49 | } 50 | } 51 | 52 | public static class Connection extends SettingsFragment{ 53 | @Override 54 | public void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | addPreferencesFromResource(R.xml.settings_connection); 57 | } 58 | 59 | @Override 60 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 61 | if (key.equals("username")) { 62 | Preference pref = findPreference(key); 63 | pref.setSummary(sharedPreferences.getString(key, "")); 64 | } 65 | } 66 | } 67 | 68 | public static class Notification extends SettingsFragment { 69 | @Override 70 | public void onCreate(Bundle savedInstanceState) { 71 | super.onCreate(savedInstanceState); 72 | addPreferencesFromResource(R.xml.settings_notification); 73 | 74 | Preference systemNotifs = findPreference("system_notifications"); 75 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 76 | systemNotifs.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 77 | @Override 78 | public boolean onPreferenceClick(Preference preference) { 79 | Intent intent = new Intent(); 80 | intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS"); 81 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 82 | intent.putExtra("app_package", getContext().getPackageName()); 83 | intent.putExtra("app_uid", getContext().getApplicationInfo().uid); 84 | intent.putExtra("android.provider.extra.APP_PACKAGE", getContext().getPackageName()); 85 | getContext().startActivity(intent); 86 | return true; 87 | } 88 | }); 89 | } else { 90 | systemNotifs.setEnabled(false); 91 | } 92 | } 93 | 94 | @Override 95 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 96 | } 97 | } 98 | 99 | public static class Looks extends SettingsFragment { 100 | @Override 101 | public void onCreate(Bundle savedInstanceState) { 102 | super.onCreate(savedInstanceState); 103 | addPreferencesFromResource(R.xml.settings_looks); 104 | } 105 | 106 | @Override 107 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 108 | if (key.equals("username")) { 109 | Preference pref = findPreference(key); 110 | pref.setSummary(sharedPreferences.getString(key, "")); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/org/shirakumo/ocelot/UpdateHandler.java: -------------------------------------------------------------------------------- 1 | package org.shirakumo.ocelot; 2 | 3 | import org.shirakumo.lichat.HandlerAdapter; 4 | import org.shirakumo.lichat.Payload; 5 | import org.shirakumo.lichat.conditions.InvalidUpdateReceived; 6 | import org.shirakumo.lichat.updates.*; 7 | 8 | public class UpdateHandler extends HandlerAdapter { 9 | 10 | private Chat chat; 11 | 12 | public UpdateHandler(Chat chat){ 13 | this.chat = chat; 14 | } 15 | 16 | public void handle(Failure update){ 17 | chat.getChannel().showText(update.clock, update.from, " ** Failure: "+update.text); 18 | } 19 | 20 | public void handle(Join update){ 21 | chat.ensureChannel(update.channel); 22 | chat.getChannel(update.channel).showText(update.clock, update.from, " ** Joined "+update.channel); 23 | } 24 | 25 | public void handle(Leave update){ 26 | chat.getChannel(update.channel).showText(update.clock, update.from, " ** Left "+update.channel); 27 | } 28 | 29 | public void handle(Message update){ 30 | chat.getChannel(update.channel).showText(update.clock, update.from, update.text); 31 | } 32 | 33 | public void handle(Data update){ 34 | chat.getChannel(update.channel).showData(update.clock, update.from, update); 35 | } 36 | 37 | public void handle(Channels update){ 38 | chat.getChannel(Chat.SYSTEM_CHANNEL).showText(update.clock, update.from, "Channels: "+ 39 | Toolkit.join(update.channels, ", ")); 40 | } 41 | 42 | public void handle(Users update){ 43 | chat.getChannel(update.from).showText(update.clock, update.from, "Users: "+ 44 | Toolkit.join(update.users, ", ")); 45 | } 46 | 47 | public void handle(Connect update){ 48 | chat.getChannel(Chat.SYSTEM_CHANNEL).showText(" ** Connection established"); 49 | } 50 | 51 | public void handle(Disconnect update){ 52 | chat.getChannel(Chat.SYSTEM_CHANNEL).showText(" ** Connection closed"); 53 | } 54 | 55 | public void handle(ConnectionLost update){ 56 | if(update.exception instanceof InvalidUpdateReceived){ 57 | Object real = ((InvalidUpdateReceived)update.exception).object; 58 | if(real instanceof Failure){ 59 | chat.getChannel(Chat.SYSTEM_CHANNEL).showText(" ** "+((Failure)real).text); 60 | }else{ 61 | chat.getChannel(Chat.SYSTEM_CHANNEL).showText(" ** Received unexpected response of type "+real.getClass().getSimpleName()); 62 | } 63 | }else{ 64 | chat.getChannel(Chat.SYSTEM_CHANNEL).showText(" ** Connection lost"); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/channel_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_circle_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_attach_file_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_exit_to_app_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_group_add_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_outline_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_input_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_insert_emoticon_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_looks_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 16 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notifications_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_people_outline_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_person_add_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_person_outline_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_remove_circle_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings_ethernet_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/white_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_chat.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 |