Private
Public Access
1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
James Magahern
40fb964cb3 SendMessage: allows creation of new conversations in addition to replying to guids 2021-07-08 15:35:29 -07:00
James Magahern
d814c2e4f6 Started working on new conversation / address validation 2021-07-08 13:46:10 -07:00
381 changed files with 751 additions and 31954 deletions

View File

@@ -1,61 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
: "${RELEASE_TAG:?Missing RELEASE_TAG}"
: "${TAG_PREFIX:?Missing TAG_PREFIX}"
: "${ASSETS_DIR:?Missing ASSETS_DIR}"
: "${RPM_BINARY_DIR:?Missing RPM_BINARY_DIR}"
: "${RPM_BINARY_PATTERN_PREFIX:?Missing RPM_BINARY_PATTERN_PREFIX}"
: "${RPM_DISTRO_NAME:?Missing RPM_DISTRO_NAME}"
: "${RPM_DISTRO_VERSION:?Missing RPM_DISTRO_VERSION}"
: "${GITHUB_ENV:?Missing GITHUB_ENV}"
version="${RELEASE_TAG#${TAG_PREFIX}}"
if [[ -z "$version" || "$version" == "$RELEASE_TAG" ]]; then
echo "Expected tag in the form ${TAG_PREFIX}{version}, got: $RELEASE_TAG" >&2
exit 1
fi
if [[ -n "${EXPECTED_VERSION:-}" && "$EXPECTED_VERSION" != "$version" ]]; then
echo "Release tag version ($version) does not match expected package version ($EXPECTED_VERSION)." >&2
exit 1
fi
rm -rf "$ASSETS_DIR"
mkdir -p "$ASSETS_DIR"
found=0
binary_pattern="${RPM_BINARY_PATTERN_PREFIX}${version}-*.rpm"
if [[ -d "$RPM_BINARY_DIR" ]]; then
find_cmd=(
find "$RPM_BINARY_DIR" -type f -name "$binary_pattern"
)
while IFS= read -r exclude; do
[[ -n "$exclude" ]] || continue
find_cmd+=( ! -name "$exclude" )
done <<< "${RPM_BINARY_EXCLUDE_PATTERNS:-}"
find_cmd+=( -exec cp '{}' "$ASSETS_DIR/" ';' )
"${find_cmd[@]}"
fi
if find "$ASSETS_DIR" -maxdepth 1 -type f -name '*.rpm' | grep -q .; then
found=1
fi
if [[ -n "${RPM_SOURCE_DIR:-}" && -n "${RPM_SOURCE_PATTERN_PREFIX:-}" && -d "$RPM_SOURCE_DIR" ]]; then
source_pattern="${RPM_SOURCE_PATTERN_PREFIX}${version}-*.src.rpm"
find "$RPM_SOURCE_DIR" -type f -name "$source_pattern" -exec cp '{}' "$ASSETS_DIR/" ';'
fi
if [[ "$found" -ne 1 ]]; then
echo "No RPM artifacts were produced." >&2
exit 1
fi
package_group="${RPM_PACKAGE_GROUP:-${RPM_DISTRO_NAME}/${RPM_DISTRO_VERSION}}"
{
printf 'RELEASE_VERSION=%s\n' "$version"
printf 'RELEASE_ASSETS_DIR=%s\n' "$ASSETS_DIR"
printf 'RPM_PACKAGE_GROUP=%s\n' "$package_group"
} >> "$GITHUB_ENV"

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
: "${GITEA_SERVER_URL:?Missing GITEA_SERVER_URL}"
: "${GITEA_REPOSITORY_OWNER:?Missing GITEA_REPOSITORY_OWNER}"
: "${RELEASE_ASSETS_DIR:?Missing RELEASE_ASSETS_DIR}"
owner="${GITEA_REPOSITORY_OWNER}"
package_user="${RPM_PACKAGE_USERNAME:-${GITEA_REPOSITORY_OWNER}}"
token="${RPM_PACKAGE_TOKEN:-}"
group="${RPM_PACKAGE_GROUP:-}"
if [[ -z "$package_user" ]]; then
echo "Missing package upload username. Set repository or organization variable RPM_PACKAGE_USERNAME." >&2
exit 1
fi
if [[ -z "$token" ]]; then
echo "Missing package upload token. Set repository or organization secret RPM_PACKAGE_TOKEN." >&2
exit 1
fi
upload_url="${GITEA_SERVER_URL%/}/api/packages/${owner}/rpm"
if [[ -n "$group" ]]; then
upload_url="${upload_url}/${group}"
fi
upload_url="${upload_url}/upload"
shopt -s nullglob
found_rpm=0
for rpm in "$RELEASE_ASSETS_DIR"/*.rpm; do
case "$rpm" in
*.src.rpm)
continue
;;
esac
found_rpm=1
http_code="$(curl --silent --show-error \
--write-out '%{http_code}' \
--output /tmp/package-upload-response \
--user "${package_user}:${token}" \
--upload-file "$rpm" \
"$upload_url")"
case "$http_code" in
201)
echo "Uploaded $(basename "$rpm") to the RPM package registry."
;;
409)
echo "Package already exists for $(basename "$rpm"); skipping duplicate upload."
;;
*)
echo "Failed to upload $(basename "$rpm") to $upload_url (HTTP $http_code)." >&2
cat /tmp/package-upload-response >&2 || true
exit 1
;;
esac
done
shopt -u nullglob
if [[ "$found_rpm" -ne 1 ]]; then
echo "No binary RPM artifacts were found to upload." >&2
exit 1
fi

View File

@@ -1,141 +0,0 @@
name: Android Release
on:
push:
tags:
- 'release/android/*'
env:
ANDROID_SDK_ROOT: ${{ gitea.workspace }}/android-sdk
ANDROID_HOME: ${{ gitea.workspace }}/android-sdk
jobs:
build-android-release:
runs-on: ubuntu-latest
steps:
# Gitea's default act_runner labels map ubuntu-latest to node:16-bullseye,
# so keep the GitHub-hosted actions on their Node 16-compatible v3 line.
- name: Install system dependencies
run: |
set -eu
apt-get update
apt-get install -y ca-certificates git openjdk-17-jdk unzip wget
- name: Check out repository code
uses: actions/checkout@v3
- name: Install Android SDK components
run: |
set -eu
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/android-commandlinetools.zip
rm -rf "$ANDROID_SDK_ROOT"
mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools"
unzip -q /tmp/android-commandlinetools.zip -d /tmp/android-commandlinetools
mv /tmp/android-commandlinetools/cmdline-tools "$ANDROID_SDK_ROOT/cmdline-tools/latest"
# sdkmanager exits successfully once it has consumed all input, which
# causes `yes` to receive SIGPIPE and return 141 under `pipefail`.
set +o pipefail
yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_SDK_ROOT" --licenses
set -o pipefail
"$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_SDK_ROOT" \
"platform-tools" \
"build-tools;33.0.1" \
"platforms;android-33"
- name: Prepare Android signing config
env:
ANDROID_RELEASE_KEYSTORE_B64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_B64 }}
ORG_GRADLE_PROJECT_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_STORE_PASSWORD }}
ORG_GRADLE_PROJECT_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }}
ORG_GRADLE_PROJECT_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }}
run: |
set -eu
: "${ANDROID_RELEASE_KEYSTORE_B64:?Missing secret ANDROID_RELEASE_KEYSTORE_B64}"
: "${ORG_GRADLE_PROJECT_RELEASE_STORE_PASSWORD:?Missing secret ANDROID_RELEASE_STORE_PASSWORD}"
: "${ORG_GRADLE_PROJECT_RELEASE_KEY_ALIAS:?Missing secret ANDROID_RELEASE_KEY_ALIAS}"
: "${ORG_GRADLE_PROJECT_RELEASE_KEY_PASSWORD:?Missing secret ANDROID_RELEASE_KEY_PASSWORD}"
keystore_path="${{ gitea.workspace }}/android-release.keystore"
printf '%s' "$ANDROID_RELEASE_KEYSTORE_B64" | base64 -d > "$keystore_path"
chmod 600 "$keystore_path"
printf 'ORG_GRADLE_PROJECT_RELEASE_STORE_FILE=%s\n' "$keystore_path" >> "$GITHUB_ENV"
printf 'ORG_GRADLE_PROJECT_RELEASE_STORE_PASSWORD=%s\n' "$ORG_GRADLE_PROJECT_RELEASE_STORE_PASSWORD" >> "$GITHUB_ENV"
printf 'ORG_GRADLE_PROJECT_RELEASE_KEY_ALIAS=%s\n' "$ORG_GRADLE_PROJECT_RELEASE_KEY_ALIAS" >> "$GITHUB_ENV"
printf 'ORG_GRADLE_PROJECT_RELEASE_KEY_PASSWORD=%s\n' "$ORG_GRADLE_PROJECT_RELEASE_KEY_PASSWORD" >> "$GITHUB_ENV"
- name: Build Android release APKs
working-directory: android
run: ./gradlew assembleRelease
- name: Prepare release assets
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -eu
version="${RELEASE_TAG#release/android/}"
if [ -z "$version" ] || [ "$version" = "$RELEASE_TAG" ]; then
echo "Expected tag in the form release/android/{version}, got: $RELEASE_TAG" >&2
exit 1
fi
assets_dir="${{ gitea.workspace }}/release-assets/android"
rm -rf "$assets_dir"
mkdir -p "$assets_dir"
found_apk=0
for apk in android/app/build/outputs/apk/release/*.apk; do
if [ ! -e "$apk" ]; then
continue
fi
found_apk=1
base="$(basename "$apk")"
case "$base" in
app-*-release*.apk)
arch="${base#app-}"
arch="${arch%%-release*}"
;;
app-release*.apk)
arch="universal"
;;
*)
echo "Unexpected APK filename: $base" >&2
exit 1
;;
esac
cp "$apk" "$assets_dir/kordophone-${arch}-${version}.apk"
done
if [ "$found_apk" -ne 1 ]; then
echo "No release APKs were produced." >&2
exit 1
fi
{
printf 'RELEASE_VERSION=%s\n' "$version"
printf 'RELEASE_ASSETS_DIR=%s\n' "$assets_dir"
} >> "$GITHUB_ENV"
- name: Create Gitea release
uses: https://gitea.com/actions/gitea-release-action@v1
with:
name: Kordophone Android ${{ env.RELEASE_VERSION }}
tag_name: ${{ github.ref_name }}
target_commitish: ${{ github.sha }}
files: |
${{ env.RELEASE_ASSETS_DIR }}/*.apk
- name: Clean up signing material
if: ${{ always() }}
run: rm -f "${{ gitea.workspace }}/android-release.keystore"

View File

@@ -1,112 +0,0 @@
name: Core RPM Release
on:
push:
tags:
- 'release/core/*'
permissions:
code: read
releases: write
packages: write
env:
RPM_DISTRO_NAME: fedora
RPM_DISTRO_VERSION: '43'
jobs:
build-core-rpm-release:
runs-on: ubuntu-latest
container:
image: fedora:43
steps:
# Build inside Fedora so the RPM package repository grouping matches the
# Fedora release we publish to.
- name: Install system dependencies
run: |
set -eu
dnf install -y \
ca-certificates \
curl \
gcc \
gcc-c++ \
git \
make \
nodejs \
openssl-devel \
pkg-config \
python3 \
rpm-build \
sqlite-devel \
dbus-devel \
systemd-devel
dnf clean all
- name: Install Rust toolchain
run: |
set -eu
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "/root/.cargo/bin" >> "$GITHUB_PATH"
- name: Install cargo-generate-rpm
run: cargo install cargo-generate-rpm
- name: Check out repository code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Build Core RPM
working-directory: core
run: make rpm
- name: Read Core Package Version
run: |
set -eu
version="$(awk -F ' = ' '
$0 == "[package]" { in_pkg = 1; next }
/^\[/ { in_pkg = 0 }
in_pkg && $1 == "version" {
gsub(/"/, "", $2)
print $2
exit
}
' core/kordophoned/Cargo.toml)"
if [ -z "$version" ]; then
echo "Could not determine kordophoned package version from core/kordophoned/Cargo.toml." >&2
exit 1
fi
printf 'CORE_PACKAGE_VERSION=%s\n' "$version" >> "$GITHUB_ENV"
- name: Prepare release assets
env:
RELEASE_TAG: ${{ github.ref_name }}
TAG_PREFIX: release/core/
ASSETS_DIR: ${{ gitea.workspace }}/release-assets/core
RPM_BINARY_DIR: ${{ gitea.workspace }}/core/target/generate-rpm
RPM_BINARY_PATTERN_PREFIX: kordophoned-
RPM_PACKAGE_GROUP: ${{ vars.RPM_PACKAGE_GROUP }}
RPM_DISTRO_NAME: ${{ env.RPM_DISTRO_NAME }}
RPM_DISTRO_VERSION: ${{ env.RPM_DISTRO_VERSION }}
EXPECTED_VERSION: ${{ env.CORE_PACKAGE_VERSION }}
run: ./.gitea/scripts/prepare-rpm-release-assets.sh
- name: Upload RPMs to Gitea package registry
env:
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_REPOSITORY_OWNER: ${{ gitea.repository_owner }}
RPM_PACKAGE_GROUP: ${{ env.RPM_PACKAGE_GROUP }}
RPM_PACKAGE_TOKEN: ${{ secrets.RPM_PACKAGE_TOKEN }}
RPM_PACKAGE_USERNAME: ${{ vars.RPM_PACKAGE_USERNAME }}
RELEASE_ASSETS_DIR: ${{ env.RELEASE_ASSETS_DIR }}
run: ./.gitea/scripts/upload-rpm-packages.sh
- name: Create Gitea release
uses: https://gitea.com/actions/gitea-release-action@v1
with:
name: Kordophoned Core ${{ env.RELEASE_VERSION }}
tag_name: ${{ github.ref_name }}
target_commitish: ${{ github.sha }}
files: |
${{ env.RELEASE_ASSETS_DIR }}/*.rpm

View File

@@ -1,97 +0,0 @@
name: GTK RPM Release
on:
push:
tags:
- 'release/gtk/*'
permissions:
code: read
releases: write
packages: write
env:
RPM_DISTRO_NAME: fedora
RPM_DISTRO_VERSION: '43'
jobs:
build-gtk-rpm-release:
runs-on: ubuntu-latest
container:
image: fedora:43
steps:
# The default Gitea runner image is Debian-based. Build the GTK RPM in a
# Fedora container so rpmbuild and the RPM build dependencies match the
# existing local packaging environment.
- name: Install system dependencies
run: |
set -eu
dnf install -y \
ca-certificates \
curl \
gcc \
git \
ImageMagick \
libadwaita-devel \
libgee-devel \
libsecret-devel \
make \
meson \
ninja-build \
nodejs \
pkgconfig \
python3 \
redhat-rpm-config \
rpm-build \
rpmdevtools \
gtk4-devel \
glib2-devel \
vala
dnf clean all
rpmdev-setuptree
- name: Check out repository code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Build GTK RPMs
working-directory: gtk
run: make rpm
- name: Prepare release assets
env:
RELEASE_TAG: ${{ github.ref_name }}
TAG_PREFIX: release/gtk/
ASSETS_DIR: ${{ gitea.workspace }}/release-assets/gtk
RPM_BINARY_DIR: /root/rpmbuild/RPMS
RPM_BINARY_PATTERN_PREFIX: kordophone-
RPM_BINARY_EXCLUDE_PATTERNS: |
*-debuginfo-*
*-debugsource-*
RPM_PACKAGE_GROUP: ${{ vars.RPM_PACKAGE_GROUP }}
RPM_SOURCE_DIR: /root/rpmbuild/SRPMS
RPM_SOURCE_PATTERN_PREFIX: kordophone-
RPM_DISTRO_NAME: ${{ env.RPM_DISTRO_NAME }}
RPM_DISTRO_VERSION: ${{ env.RPM_DISTRO_VERSION }}
run: ./.gitea/scripts/prepare-rpm-release-assets.sh
- name: Upload RPMs to Gitea package registry
env:
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_REPOSITORY_OWNER: ${{ gitea.repository_owner }}
RPM_PACKAGE_GROUP: ${{ env.RPM_PACKAGE_GROUP }}
RPM_PACKAGE_TOKEN: ${{ secrets.RPM_PACKAGE_TOKEN }}
RPM_PACKAGE_USERNAME: ${{ vars.RPM_PACKAGE_USERNAME }}
RELEASE_ASSETS_DIR: ${{ env.RELEASE_ASSETS_DIR }}
run: ./.gitea/scripts/upload-rpm-packages.sh
- name: Create Gitea release
uses: https://gitea.com/actions/gitea-release-action@v1
with:
name: Kordophone GTK ${{ env.RELEASE_VERSION }}
tag_name: ${{ github.ref_name }}
target_commitish: ${{ github.sha }}
files: |
${{ env.RELEASE_ASSETS_DIR }}/*.rpm

3
.gitignore vendored
View File

@@ -1,3 +0,0 @@
.codex
ext/
target/

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "CocoaHTTPServer"]
path = server/CocoaHTTPServer
path = CocoaHTTPServer
url = https://github.com/robbiehanson/CocoaHTTPServer.git

View File

@@ -1754,13 +1754,10 @@ __attribute__((visibility("default"))) @interface IMService : NSObject { }
@property(nonatomic) BOOL chatInScrutinyMode; // @synthesize chatInScrutinyMode=_chatInScrutinyMode;
@property(readonly, nonatomic) NSArray *messageEditChatItems; // @synthesize messageEditChatItems=_messageEditChatItems;
@property(retain, nonatomic, setter=_setVisibleAssociatedMessageChatItems:) NSArray *visibleAssociatedMessageChatItems; // @synthesize visibleAssociatedMessageChatItems=_visibleAssociatedMessageChatItems;
@property(nonatomic) struct _NSRange messagePartRange; // @synthesize messagePartRange=_messagePartRange;
@property(nonatomic) long long index; // @synthesize index=_index;
@property(readonly, copy, nonatomic) NSAttributedString *text; // @synthesize text=_text;
@property (nonatomic, copy, readonly) NSString *threadIdentifier;
@property(nonatomic) struct _NSRange messagePartRange; // @synthesize messagePartRange=_messagePartRange;
- (BOOL)canSendMessageAcknowledgment;
- (void)_setMessageEditChatItems:(id)arg1;
- (id)_initWithItem:(id)arg1 text:(id)arg2 index:(long long)arg3 messagePartRange:(struct _NSRange)arg4 visibleAssociatedMessageChatItems:(id)arg5;
@@ -3460,7 +3457,6 @@ __attribute__((visibility("default"))) @interface IMService : NSObject { }
- (id)loadUnreadMessagesWithLimit:(unsigned long long)arg1 fallbackToMessagesUpToGUID:(id)arg2;
- (id)loadFrequentRepliesLimit:(unsigned long long)arg1 loadImmediately:(BOOL)arg2;
- (id)loadMessagesBeforeAndAfterGUID:(id)arg1 numberOfMessagesToLoadBeforeGUID:(unsigned long long)arg2 numberOfMessagesToLoadAfterGUID:(unsigned long long)arg3 loadImmediately:(BOOL)arg4;
- (id)loadMessagesBeforeAndAfterGUID:(id)arg1 numberOfMessagesToLoadBeforeGUID:(unsigned long long)arg2 numberOfMessagesToLoadAfterGUID:(unsigned long long)arg3 loadImmediately:(BOOL)arg4 threadIdentifier:(id)tid;
- (id)loadMessagesUpToGUID:(id)arg1 date:(id)arg2 limit:(unsigned long long)arg3 loadImmediately:(BOOL)arg4;
- (id)loadMessagesBeforeDate:(id)arg1 limit:(unsigned long long)arg2 loadImmediately:(BOOL)arg3;
- (id)loadMessagesBeforeDate:(id)arg1 limit:(unsigned long long)arg2;
@@ -3683,68 +3679,6 @@ __attribute__((visibility("default"))) @interface IMService : NSObject { }
@end
typedef NS_ENUM(NSInteger, IMMessageDescriptionType) {
IMMessageDescriptionAccessibility,
IMMessageDescriptionAcknowledgement,
IMMessageDescriptionConversationList,
IMMessageDescriptionNotification,
IMMessageDescriptionSiri,
IMMessageDescriptionSPI,
};
typedef NS_ENUM(int64_t, IMAssociatedMessageType) {
IMAssociatedMessageTypeUnspecified = 0,
IMAssociatedMessageTypeBreadcrumbUnconsumed = 2,
IMAssociatedMessageTypeBreadcrumbConsumed = 3,
IMAssociatedMessageTypeSticker = 1000,
IMAssociatedMessageTypeEmojiSticker = 1001,
IMAssociatedMessageTypeAcknowledgmentHeart = 2000,
IMAssociatedMessageTypeAcknowledgmentThumbsUp = 2001,
IMAssociatedMessageTypeAcknowledgmentThumbsDown = 2002,
IMAssociatedMessageTypeAcknowledgmentHa = 2003,
IMAssociatedMessageTypeAcknowledgmentExclamation = 2004,
IMAssociatedMessageTypeAcknowledgmentQuestionMark = 2005,
IMAssociatedMessageTypeAcknowledgmentHeartRemoved = 3000,
IMAssociatedMessageTypeAcknowledgmentThumbsUpRemoved = 3001,
IMAssociatedMessageTypeAcknowledgmentThumbsDownRemoved = 3002,
IMAssociatedMessageTypeAcknowledgmentHaRemoved = 3003,
IMAssociatedMessageTypeAcknowledgmentExclamationRemoved = 3004,
IMAssociatedMessageTypeAcknowledgmentQuestionMarkRemoved = 3005,
};
extern NSString * const IMMessageSummaryInfoSummary;
extern NSString * const IMMessageSummaryInfoContentType;
extern NSString * const IMMessageSummaryInfoPluginBundleID;
extern NSString * const IMMessageSummaryInfoPluginDisplayName;
extern NSString * const IMMessageSummaryInfoAssociatedBalloonBundleID;
extern NSString * const IMMessageSummaryInfoSourceApplicationID;
extern NSString * const IMMessageSummaryInfoUpdatedDateWithServerTime;
extern NSString * const IMMessageSummaryInfoHasBeenRetried;
extern NSString * const IMMessageSummaryInfoTapbackRepresentationKey;
extern NSString * const IMMessageSummaryInfoStickerRepositioningKey;
extern NSString * const IMMessageSummaryInfoTranscriptSharingMessageTypeKey;
extern NSString * const IMMessageSummaryInfoTranscriptSharingMessageFirstItemKey;
extern NSString * const IMMessageSummaryInfoEditedPartIndexes;
extern NSString * const IMMessageSummaryInfoRetractedPartIndexes;
extern NSString * const IMMessageSummaryInfoOriginalTextRangesByPartIndex;
extern NSString * const IMMessageSummaryInfoOriginalTextRangeLocationSubKey;
extern NSString * const IMMessageSummaryInfoOriginalTextRangeLengthSubKey;
extern NSString * const IMMessageSummaryInfoEditDeliveryFailedForPartIndexes;
extern NSString * const IMMessageSummaryInfoRetractDeliveryFailedForPartIndexes;
extern NSString * const IMMessageSummaryInfoEditUnsupportedByHandleIDs;
extern NSString * const IMMessageSummaryInfoTranslatedText;
extern NSString * const IMMessageSummaryInfoDetectedLanguage;
extern NSString * const IMMessageSummaryInfoCMMState;
extern NSString * const IMMessageSummaryInfoCMMAssetOffset;
extern NSString * const IMMessageItemErrorDomain;
extern NSString * const IMMessageSentDistributedNotification;
extern NSString * const IMMessageSentDistributedNotificationUserInfoMessageGUIDKey;
extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
@interface IMMessage : NSObject <NSCopying>
{
IMHandle *_sender;
@@ -3800,22 +3734,6 @@ extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
+ (id)breadcrumbMessageWithText:(id)arg1 associatedMessageGUID:(id)arg2 balloonBundleID:(id)arg3 fileTransferGUIDs:(id)arg4 payloadData:(id)arg5;
+ (id)editedMessageWithOriginalMessage:(id)arg1 originalPrefixedGUID:(id)arg2 newBody:(id)arg3;
+ (id)instantMessageWithAssociatedMessageContent:(id)arg1 flags:(unsigned long long)arg2 associatedMessageGUID:(id)arg3 associatedMessageType:(long long)arg4 associatedMessageRange:(struct _NSRange)arg5 messageSummaryInfo:(id)arg6;
+ (instancetype)instantMessageWithAssociatedMessageContent:(NSAttributedString *) content
flags:(IMMessageFlags)flags
associatedMessageGUID:(NSString*) associatedMessageGUID
associatedMessageType:(IMAssociatedMessageType) associatedMessageType
associatedMessageRange:(NSRange) associatedMessageRange
messageSummaryInfo:(NSDictionary*) messageSummaryInfo
threadIdentifier:(NSString *)threadIdentifier;
+ (instancetype)breadcrumbMessageWithText:(NSAttributedString *)text
associatedMessageGUID:(NSString *)messageGUID
balloonBundleID:(NSString *)balloonBundleID
fileTransferGUIDs:(NSArray<NSString *> *)fileTransferGUIDs
payloadData:(NSData *)payloadData
threadIdentifier:(NSString *)threadIdentifier;
@property(nonatomic) unsigned long long sortID; // @synthesize sortID=_sortID;
@property(nonatomic) BOOL isSOS; // @synthesize isSOS=_isSOS;
@property(retain, nonatomic) NSString *notificationIDSTokenURI; // @synthesize notificationIDSTokenURI=_notificationIDSTokenURI;
@@ -3885,8 +3803,8 @@ extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
- (id)_initWithSender:(id)arg1 time:(id)arg2 timeRead:(id)arg3 timeDelivered:(id)arg4 timePlayed:(id)arg5 plainText:(id)arg6 text:(id)arg7 messageSubject:(id)arg8 fileTransferGUIDs:(id)arg9 flags:(unsigned long long)arg10 error:(id)arg11 guid:(id)arg12 messageID:(long long)arg13 subject:(id)arg14 balloonBundleID:(id)arg15 payloadData:(id)arg16 expressiveSendStyleID:(id)arg17 timeExpressiveSendPlayed:(id)arg18 associatedMessageGUID:(id)arg19 associatedMessageType:(long long)arg20 associatedMessageRange:(struct _NSRange)arg21 messageSummaryInfo:(id)arg22;
- (id)_copyWithFlags:(unsigned long long)arg1;
- (id)copyWithZone:(struct _NSZone *)arg1;
- (id)descriptionForPurpose:(IMMessageDescriptionType)arg1 inChat:(id)arg2;
- (id)descriptionForPurpose:(IMMessageDescriptionType)arg1;
- (id)descriptionForPurpose:(long long)arg1 inChat:(id)arg2;
- (id)descriptionForPurpose:(long long)arg1;
- (void)_ovverrideGUIDForTest:(id)arg1;
@property(readonly, nonatomic) BOOL isAssociatedMessage;
- (id)initWithSender:(id)arg1 time:(id)arg2 text:(id)arg3 messageSubject:(id)arg4 fileTransferGUIDs:(id)arg5 flags:(unsigned long long)arg6 error:(id)arg7 guid:(id)arg8 subject:(id)arg9 associatedMessageGUID:(id)arg10 associatedMessageType:(long long)arg11 associatedMessageRange:(struct _NSRange)arg12 associatedMessageInfo:(id)arg13;
@@ -4161,31 +4079,39 @@ extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
@property(readonly) unsigned long long hash;
@property(readonly) Class superclass;
- (IMChat *)chatWithHandle:(IMHandle *)handle;
- (IMChat *)chatWithHandle:(IMHandle *)handle lastAddressedHandle:( NSString *)lastAddressedHandle lastAddressedSIMID:( NSString *)lastAddressedSIMID;
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles;
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles lastAddressedHandle:( NSString *)lastAddressedHandle lastAddressedSIMID:( NSString *)lastAddressedSIMID;
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles displayName:( NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly;
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles displayName:( NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly lastAddressedHandle:( NSString *)lastAddressedHandle lastAddressedSIMID:( NSString *)lastAddressedSIMID;
@end
// Monterey
#if 0
@interface IMChatRegistry ()
- (IMChat *)chatWithHandle:(IMHandle *)handle;
- (IMChat *)chatWithHandle:(IMHandle *)handle lastAddressedHandle:(NSString *)lastAddressedHandle lastAddressedSIMID:(NSString *)lastAddressedSIMID;
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles;
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles lastAddressedHandle:(NSString *)lastAddressedHandle lastAddressedSIMID:(NSString *)lastAddressedSIMID;
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles displayName:(NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly;
- (IMChat *)chatWithHandles:(NSArray<IMHandle *> *)handles displayName:(NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly lastAddressedHandle:(NSString *)lastAddressedHandle lastAddressedSIMID:(NSString *)lastAddressedSIMID;
- (NSArray<NSString *> *)allGUIDsForChat:(IMChat *)chat;
- ( IMChat *)existingChatWithHandle:(IMHandle *)handle;
- ( IMChat *)existingChatWithHandle:(IMHandle *)handle allowAlternativeService:(BOOL)allowAlternativeService;
- ( IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles;
- ( IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles allowAlternativeService:(BOOL)allowAlternativeService;
- ( IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles allowAlternativeService:(BOOL)allowAlternativeService groupID:(NSString *)groupID;
- ( IMChat *)existingChatWithHandles:(NSArray *)handles allowAlternativeService:(BOOL)allowAlternativeService groupID:( NSString *)groupID displayName:( NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly;
// - ( IMChat *)existingChatWithGUID:(NSString *)guid;
- ( IMChat *)existingChatWithPinningIdentifier:(NSString *)pinningIdentifier;
- ( IMChat *)existingChatWithDeviceIndependentID:(NSString *)deviceIndependentID;
// - ( IMChat *)existingChatWithChatIdentifier:(NSString *)identifier;
- ( IMChat *)existingChatWithPersonID:(NSString *)personID;
// - ( IMChat *)existingChatWithGroupID:(NSString *)groupID;
// - ( IMChat *)existingChatWithDisplayName:(NSString *)displayName;
- ( IMChat *)existingChatWithAddresses:(NSArray<NSString *> *)addresses allowAlternativeService:(BOOL)allowAlternativeService bestHandles:(NSArray<IMHandle *> * __autoreleasing*)outBestHandles;
- ( IMChat *)existingChatWithContacts:(NSSet /* <CNContact *> */ *)contacts bestHandles:(NSArray<IMHandle *> * __autoreleasing * )outBestHandles;
- (IMChat *)existingChatWithHandle:(IMHandle *)handle;
- (IMChat *)existingChatWithHandle:(IMHandle *)handle allowAlternativeService:(BOOL)allowAlternativeService;
- (IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles;
- (IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles allowAlternativeService:(BOOL)allowAlternativeService;
- (IMChat *)existingChatWithHandles:(NSArray<IMHandle *> *)handles allowAlternativeService:(BOOL)allowAlternativeService groupID:(NSString *)groupID;
- (IMChat *)existingChatWithHandles:(NSArray *)handles allowAlternativeService:(BOOL)allowAlternativeService groupID:(NSString *)groupID displayName:(NSString *)displayName joinedChatsOnly:(BOOL)joinedChatsOnly;
- (IMChat *)existingChatWithPinningIdentifier:(NSString *)pinningIdentifier;
- (IMChat *)existingChatWithDeviceIndependentID:(NSString *)deviceIndependentID;
- (IMChat *)existingChatWithPersonID:(NSString *)personID;
- (IMChat *)existingChatWithDisplayName:(NSString *)displayName;
- (IMChat *)existingChatWithAddresses:(NSArray<NSString *> *)addresses allowAlternativeService:(BOOL)allowAlternativeService bestHandles:(NSArray<IMHandle *> **)outBestHandles;
- (IMChat *)existingChatWithContacts:(NSSet *)contacts bestHandles:(NSArray<IMHandle *> **)outBestHandles;
@end
#endif
@interface IMSimulatedChat : IMChat // <IMSimulatedChatDelegate, IMSimulatedDaemonListener>
{
@@ -5080,66 +5006,3 @@ extern NSString * const IMMessageSentDistributedNotificationUserInfoSucessKey;
- (struct __CFArray *)copyDDResultArrayByScanningStringForURLs;
@end
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, FZIDType) {
FZUnknownIDType = -1,
FZPhoneNumberBasedID = 0,
FZEmailBasedID = 1,
FZDSBasedID = 2,
FZBusinessBasedID = 3,
FZTemporaryBasedID = 4,
FZPseudonymBasedID = 5,
FZHardwareBasedID = 6,
FZSIPBasedID = 7,
};
@interface NSString (FezAdditions)
- (FZIDType) _FZIDType;
- (FZIDType) _FZBestGuessFZIDType;
- (NSString *) _IDFromFZIDType:(FZIDType)IDType;
- (NSString *) _stripFZIDPrefix;
- (NSString *) _URIFromFZIDType:(FZIDType)IDType;
- (NSString *) _bestGuessURI;
@end
extern BOOL IMStringIsEmail( NSString * string );
extern BOOL IMStringIsPhoneNumber( NSString * string );
extern BOOL IMStringIsBusinessID( NSString * string );
extern BOOL IMStringIsTemporaryID( NSString * string );
extern BOOL IMStringIsPseudonymID( NSString * string );
extern BOOL IMStringIsHardwareID( NSString * string );
extern BOOL IMStringIsSIPID( NSString * string );
enum {
IMChatServiceForSendingAvailabilityErrorNone = 0,
IMChatServiceForSendingAvailabilityErrorTooManyRecipients = 1,
IMChatServiceForSendingAvailabilityErrorIMessageRequired = 2,
IMChatServiceForSendingAvailabilityErrorMMSRequired = 3,
IMChatServiceForSendingAvailabilityErrorNoAvailableServices = 4,
IMChatServiceForSendingAvailabilityErrorSpamFiltered = 5,
};
typedef int8_t IMChatServiceForSendingAvailabilityError;
extern void IMChatCalculateServiceForSendingNewComposeMaybeForce(NSArray<NSString *> *canonicalIDSAddresses,
NSString* _Nullable senderLastAddressedHandle,
NSString* _Nullable senderLastAddressedSIMID,
BOOL forceMMS,
BOOL hasEmailRecipients,
BOOL lastSentMessageWasNotDelivered,
BOOL conversationWasDowngraded,
BOOL hasConversationHistory,
IMService * _Nullable previousService,
void(^ _Nonnull completion)(BOOL allAddressesiMessageCapable,
NSDictionary * _Nullable perRecipientAvailability,
BOOL checkedServer,
IMChatServiceForSendingAvailabilityError error));
extern NSString *IMCopyIDForPhoneNumber(NSString *phoneNumber, NSString *_Nullable countryCode, BOOL useNetworkCountryCode) NS_RETURNS_RETAINED;
extern NSString *IMCopyIDForEmailAddress(NSString *emailAddress) NS_RETURNS_RETAINED;
NS_ASSUME_NONNULL_END

View File

@@ -1898,3 +1898,29 @@ typedef void (^CDUnknownBlockType)(void); // return type and parameters are unkn
- (void)differencesFromArray:(id)arg1 removedIndexes:(id *)arg2 insertedIndexes:(id *)arg3;
@end
extern BOOL IMStringIsEmail (NSString *string);
extern NSString* IMStripFormattingFromAddress (NSString *address);
typedef void(^IMChatCalculateServiceForSendingCompletionBlock) (BOOL allAddressesiMessageCapable,
NSDictionary *availabilityPerRecipient,
BOOL checkedServer,
NSError *error);
extern void IMChatCalculateServiceForSendingNewComposeMaybeForce (NSArray *addresses,
NSString *senderLastAddressedHandle,
NSString *senderLastAddressedSIMID,
BOOL forceMMS,
BOOL hasEmailRecipients,
BOOL lastSentMessageWasNotDelivered,
BOOL conversationWasDowngraded,
BOOL hasConversationHistory,
IMService *previousService,
IMChatCalculateServiceForSendingCompletionBlock completion);
// IDS
extern NSString *IDSCopyIDForPhoneNumber(NSString *phoneNumber);
extern NSString *IDSCopyIDForEmailAddress(NSString *emailAddress);

View File

@@ -18,18 +18,12 @@ struct IMFileLocation_t {
int _field5;
};
typedef struct IMPreviewConstraints {
CGFloat maxPxWidth;
CGSize minThumbnailPxSize;
CGFloat scale;
BOOL isSticker;
BOOL generateMetadata;
} IMPreviewConstraints;
extern IMPreviewConstraints IMPreviewConstraintsFromDictionary(NSDictionary *dictionary);
extern NSDictionary *IMPreviewConstraintsDictionaryFromConstraint(IMPreviewConstraints constraint);
extern BOOL IMPreviewConstraintsEqualToConstraints(IMPreviewConstraints constraints1, IMPreviewConstraints constraints2);
extern IMPreviewConstraints IMPreviewConstraintsZero(void);
struct IMPreviewConstraints {
double _field1;
struct CGSize _field2;
double _field3;
char _field4;
};
struct _TidyDoc {
int _field1;
@@ -161,8 +155,6 @@ struct __va_list_tag {
- (void)setObject:(id)arg1 forKey:(id)arg2;
@end
extern NSURL* IMAttachmentPreviewFileURL(NSURL *attachmentURL, NSString *extension, BOOL generateIntermediaryDirectories);
@protocol IMPreviewGeneratorProtocol
+ (BOOL)shouldShadePreview;
+ (BOOL)shouldScaleUpPreview;
@@ -753,19 +745,6 @@ extern NSURL* IMAttachmentPreviewFileURL(NSURL *attachmentURL, NSString *extensi
@end
@interface IMImageUtilities : NSObject
{
}
+ (struct CGImage *)newThumbnailForTargetSize:(struct CGSize)arg1 imageSize:(struct CGSize)arg2 imageSource:(struct CGImageSource *)arg3 atIndex:(unsigned long long)arg4 mode:(long long)arg5 scale:(double)arg6;
+ (struct CGImage *)newThumbnailForTargetSize:(struct CGSize)arg1 imageSize:(struct CGSize)arg2 imageSource:(struct CGImageSource *)arg3 mode:(long long)arg4 scale:(double)arg5;
+ (BOOL)persistCPBitmapWithImage:(struct CGImage *)arg1 url:(id)arg2;
+ (void)sampleImageEdges:(char *)arg1 usingRect:(struct CGRect)arg2 forMostlyWhitePixels:(unsigned long long *)arg3 otherPixels:(unsigned long long *)arg4 bytesPerRow:(long long)arg5;
+ (struct CGSize)imageRefPxSize:(struct CGImage *)arg1;
+ (struct CGSize)imageSourcePxSize:(struct CGImageSource *)arg1;
@end
@interface IMOneTimeCodeUtilities : NSObject
{
}
@@ -1434,17 +1413,6 @@ extern NSURL* IMAttachmentPreviewFileURL(NSURL *attachmentURL, NSString *extensi
@end
@interface IMImagePreviewGenerator : IMPreviewGenerator <IMPreviewGeneratorProtocol, IMUTITypeInformation>
{
}
+ (struct CGImage *)newThumbnailFillToSize:(struct CGSize)arg1 imagePxSize:(struct CGSize)arg2 imageSource:(struct CGImageSource *)arg3 scale:(double)arg4;
+ (struct CGImage *)newPreviewFromSourceURL:(id)arg1 withPreviewConstraints:(struct IMPreviewConstraints)arg2 error:(id *)arg3;
+ (id)UTITypes;
+ (id)fetchUTITypes;
@end
@interface IMAKAppleIDAuthenticationController : NSObject
{
@@ -1704,4 +1672,3 @@ extern NSURL* IMAttachmentPreviewFileURL(NSURL *attachmentURL, NSString *extensi
- (id)initWithSerializedError_im:(struct NSDictionary *)arg1;
@end
extern NSString *IMAssociatedMessageDecodeGUID(NSString* guid);

View File

@@ -1,6 +1,5 @@
INSTALL_PATH := /usr/share/kordophone
.PHONY: build/Release/kordophoned
build/Release/kordophoned:
xcodebuild
@@ -8,6 +7,7 @@ build/Release/kordophoned:
install: build/Release/kordophoned
install -d $(INSTALL_PATH)
install build/Release/kordophoned $(INSTALL_PATH)
cp -rf build/Release/CocoaHTTPServer.framework $(INSTALL_PATH)
clean:
rm -Rf build

View File

@@ -14,61 +14,79 @@
1AA43E95219EC38E00EDF1A7 /* MBIMHTTPUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AA43E94219EC38E00EDF1A7 /* MBIMHTTPUtilities.m */; };
1AAB32B121F82EB7004A2A72 /* MBIMLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AAB32B021F82EB7004A2A72 /* MBIMLogging.m */; };
1AAB32B421F837BB004A2A72 /* hookAgent.sh in CopyFiles */ = {isa = PBXBuildFile; fileRef = CD83E1B5219BF78E00F4CCEA /* hookAgent.sh */; };
1ACFCF2A219EB2AC00E2C237 /* HTTPConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE00219EB2AB00E2C237 /* HTTPConnection.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF2B219EB2AC00E2C237 /* HTTPLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE01219EB2AB00E2C237 /* HTTPLogging.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF2C219EB2AC00E2C237 /* HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE02219EB2AB00E2C237 /* HTTPMessage.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF2D219EB2AC00E2C237 /* WebSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE03219EB2AB00E2C237 /* WebSocket.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF2E219EB2AC00E2C237 /* HTTPAuthenticationRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE04219EB2AB00E2C237 /* HTTPAuthenticationRequest.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF2F219EB2AC00E2C237 /* HTTPAsyncFileResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE06219EB2AB00E2C237 /* HTTPAsyncFileResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF30219EB2AC00E2C237 /* HTTPErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE07219EB2AB00E2C237 /* HTTPErrorResponse.m */; };
1ACFCF31219EB2AC00E2C237 /* HTTPDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE08219EB2AB00E2C237 /* HTTPDataResponse.m */; };
1ACFCF32219EB2AC00E2C237 /* HTTPRedirectResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE09219EB2AB00E2C237 /* HTTPRedirectResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF33219EB2AC00E2C237 /* HTTPDynamicFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0A219EB2AB00E2C237 /* HTTPDynamicFileResponse.m */; };
1ACFCF34219EB2AC00E2C237 /* HTTPFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0B219EB2AB00E2C237 /* HTTPFileResponse.m */; };
1ACFCF35219EB2AC00E2C237 /* HTTPAsyncFileResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0C219EB2AB00E2C237 /* HTTPAsyncFileResponse.m */; };
1ACFCF36219EB2AC00E2C237 /* HTTPRedirectResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE0D219EB2AB00E2C237 /* HTTPRedirectResponse.m */; };
1ACFCF37219EB2AC00E2C237 /* HTTPDataResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE0E219EB2AB00E2C237 /* HTTPDataResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF38219EB2AC00E2C237 /* HTTPErrorResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE0F219EB2AB00E2C237 /* HTTPErrorResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF39219EB2AC00E2C237 /* HTTPFileResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE10219EB2AB00E2C237 /* HTTPFileResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF3A219EB2AC00E2C237 /* HTTPDynamicFileResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE11219EB2AB00E2C237 /* HTTPDynamicFileResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF3B219EB2AC00E2C237 /* HTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE12219EB2AB00E2C237 /* HTTPServer.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF3C219EB2AC00E2C237 /* HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE13219EB2AB00E2C237 /* HTTPMessage.m */; };
1ACFCF3D219EB2AC00E2C237 /* HTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE14219EB2AB00E2C237 /* HTTPConnection.m */; };
1ACFCF3E219EB2AC00E2C237 /* WebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE15219EB2AB00E2C237 /* WebSocket.m */; };
1ACFCF3F219EB2AC00E2C237 /* HTTPResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE16219EB2AB00E2C237 /* HTTPResponse.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF40219EB2AC00E2C237 /* MultipartFormDataParser.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE18219EB2AB00E2C237 /* MultipartFormDataParser.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF41219EB2AC00E2C237 /* MultipartMessageHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE19219EB2AB00E2C237 /* MultipartMessageHeader.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF42219EB2AC00E2C237 /* MultipartMessageHeaderField.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1A219EB2AB00E2C237 /* MultipartMessageHeaderField.m */; };
1ACFCF43219EB2AC00E2C237 /* MultipartFormDataParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1B219EB2AB00E2C237 /* MultipartFormDataParser.m */; };
1ACFCF44219EB2AC00E2C237 /* MultipartMessageHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1C219EB2AB00E2C237 /* MultipartMessageHeader.m */; };
1ACFCF45219EB2AC00E2C237 /* MultipartMessageHeaderField.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE1D219EB2AB00E2C237 /* MultipartMessageHeaderField.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF46219EB2AC00E2C237 /* HTTPAuthenticationRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE1E219EB2AB00E2C237 /* HTTPAuthenticationRequest.m */; };
1ACFCF47219EB2AC00E2C237 /* DDNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE20219EB2AB00E2C237 /* DDNumber.m */; };
1ACFCF48219EB2AC00E2C237 /* DDData.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE21219EB2AB00E2C237 /* DDData.m */; };
1ACFCF49219EB2AC00E2C237 /* DDRange.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE22219EB2AB00E2C237 /* DDRange.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF4A219EB2AC00E2C237 /* DDNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE23219EB2AB00E2C237 /* DDNumber.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF4B219EB2AC00E2C237 /* DDRange.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE24219EB2AB00E2C237 /* DDRange.m */; };
1ACFCF4C219EB2AC00E2C237 /* DDData.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCE25219EB2AB00E2C237 /* DDData.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCF4D219EB2AC00E2C237 /* HTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCE26219EB2AB00E2C237 /* HTTPServer.m */; };
1ACFCFCA219EB2AC00E2C237 /* DDTTYLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF09219EB2AC00E2C237 /* DDTTYLogger.m */; };
1ACFCFCB219EB2AC00E2C237 /* DDLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF0A219EB2AC00E2C237 /* DDLog.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCFCC219EB2AC00E2C237 /* DDAbstractDatabaseLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF0B219EB2AC00E2C237 /* DDAbstractDatabaseLogger.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCFCD219EB2AC00E2C237 /* DDASLLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF0C219EB2AC00E2C237 /* DDASLLogger.m */; };
1ACFCFCE219EB2AC00E2C237 /* DDFileLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF0D219EB2AC00E2C237 /* DDFileLogger.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCFCF219EB2AC00E2C237 /* ContextFilterLogFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF0F219EB2AC00E2C237 /* ContextFilterLogFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCFD0219EB2AC00E2C237 /* DispatchQueueLogFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF10219EB2AC00E2C237 /* DispatchQueueLogFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCFD1219EB2AC00E2C237 /* ContextFilterLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF11219EB2AC00E2C237 /* ContextFilterLogFormatter.m */; };
1ACFCFD2219EB2AC00E2C237 /* DispatchQueueLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF12219EB2AC00E2C237 /* DispatchQueueLogFormatter.m */; };
1ACFCFD4219EB2AC00E2C237 /* DDLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF14219EB2AC00E2C237 /* DDLog.m */; };
1ACFCFD6219EB2AC00E2C237 /* DDTTYLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF16219EB2AC00E2C237 /* DDTTYLogger.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCFD7219EB2AC00E2C237 /* DDAbstractDatabaseLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF17219EB2AC00E2C237 /* DDAbstractDatabaseLogger.m */; };
1ACFCFD8219EB2AC00E2C237 /* DDFileLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF18219EB2AC00E2C237 /* DDFileLogger.m */; };
1ACFCFD9219EB2AC00E2C237 /* DDASLLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF19219EB2AC00E2C237 /* DDASLLogger.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCFDA219EB2AC00E2C237 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACFCF1B219EB2AC00E2C237 /* GCDAsyncSocket.m */; };
1ACFCFDF219EB31400E2C237 /* libCocoaHTTPServer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ACFCDE2219EB28A00E2C237 /* libCocoaHTTPServer.a */; };
1ACFCFDC219EB2AC00E2C237 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACFCF1D219EB2AC00E2C237 /* GCDAsyncSocket.h */; settings = {ATTRIBUTES = (Public, ); }; };
1ACFCFDF219EB31400E2C237 /* CocoaHTTPServer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ACFCDE2219EB28A00E2C237 /* CocoaHTTPServer.framework */; };
1AD8936E21EFD986009B599A /* MBIMUploadAttachmentOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AD8936D21EFD986009B599A /* MBIMUploadAttachmentOperation.m */; };
CD14F18E219E2DB400E7DD22 /* CryptoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F18D219E2DB400E7DD22 /* CryptoTests.m */; };
CD14F1A1219FE7D600E7DD22 /* MBIMUpdatePollOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A0219FE7D600E7DD22 /* MBIMUpdatePollOperation.m */; };
CD14F1A4219FF22700E7DD22 /* IMMessageItem+Encoded.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A3219FF22700E7DD22 /* IMMessageItem+Encoded.m */; };
CD14F1AA219FF3B800E7DD22 /* MBIMUpdateQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1A9219FF3B800E7DD22 /* MBIMUpdateQueue.m */; };
CD14F1AD219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */; };
CD2782BC29527FE500C0C030 /* IMSharedUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD2782BB29527FE500C0C030 /* IMSharedUtilities.framework */; };
CD2782BF2952832B00C0C030 /* MBIMImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2782BE2952832B00C0C030 /* MBIMImageUtils.m */; };
CD2782FE2952875F00C0C030 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD2782FD2952875F00C0C030 /* CoreGraphics.framework */; };
CD2783002952876700C0C030 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD2782FF2952876700C0C030 /* ImageIO.framework */; };
CD2ECEC2269539100055E302 /* MBIMAuthenticateOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */; };
CD2ECEC526953F2A0055E302 /* MBIMAuthToken.m in Sources */ = {isa = PBXBuildFile; fileRef = CD2ECEC426953F2A0055E302 /* MBIMAuthToken.m */; };
CD3F62B1297769F2004305D9 /* MBIMURLUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = CD3F62B0297769F2004305D9 /* MBIMURLUtilities.m */; };
CD602056219B5DFD0024D9C5 /* MBIMBridgeOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */; };
CD60205C219B623F0024D9C5 /* MBIMMessagesListOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60205B219B623F0024D9C5 /* MBIMMessagesListOperation.m */; };
CD60205F219B674B0024D9C5 /* MBIMConversationListOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD60205E219B674B0024D9C5 /* MBIMConversationListOperation.m */; };
CD602062219B68950024D9C5 /* MBIMSendMessageOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD602061219B68950024D9C5 /* MBIMSendMessageOperation.m */; };
CD83E156219BE10A00F4CCEA /* hooking.m in Sources */ = {isa = PBXBuildFile; fileRef = CD83E155219BE10A00F4CCEA /* hooking.m */; };
CD83E166219BE91600F4CCEA /* agentHook.m in Sources */ = {isa = PBXBuildFile; fileRef = CD83E165219BE91600F4CCEA /* agentHook.m */; };
CD936A32289B353F0093A1AC /* MBIMErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = CD936A31289B353F0093A1AC /* MBIMErrorResponse.m */; };
CD936A35289B47D60093A1AC /* MBIMVersionOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD936A34289B47D50093A1AC /* MBIMVersionOperation.m */; };
CD936A39289B49FC0093A1AC /* MBIMStatusOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CD936A38289B49FC0093A1AC /* MBIMStatusOperation.m */; };
CDA64B472DFCBF3000E9B07E /* MBIMPingPongWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA64B462DFCBF3000E9B07E /* MBIMPingPongWebSocket.m */; };
CDDCF78D283F398C0087ABDF /* MBIMDeleteConversationOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDDCF78C283F398C0087ABDF /* MBIMDeleteConversationOperation.m */; };
CDD8C98426977D2A00551AE5 /* MBIMAliasValidationOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDD8C98326977D2A00551AE5 /* MBIMAliasValidationOperation.m */; };
CDD8C9862697996800551AE5 /* IDS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDD8C9852697996700551AE5 /* IDS.framework */; };
CDE4556421A3578A0041F5DD /* IMChat+Encoded.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE4556321A3578A0041F5DD /* IMChat+Encoded.m */; };
CDE455A121A365AD0041F5DD /* MBIMMarkOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A021A365AD0041F5DD /* MBIMMarkOperation.m */; };
CDE455A421A5308D0041F5DD /* MBIMFetchAttachmentOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A321A5308D0041F5DD /* MBIMFetchAttachmentOperation.m */; };
CDE455A721A531ED0041F5DD /* MBIMDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = CDE455A621A531ED0041F5DD /* MBIMDataResponse.m */; };
CDEFF9FE2CACC7A700063C52 /* MBIMResolveHandleOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = CDEFF9FD2CACC7A700063C52 /* MBIMResolveHandleOperation.m */; };
CDF62335219A895D00690038 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CDF62334219A895D00690038 /* main.m */; };
CDF62339219A8A5600690038 /* MBIMBridge.h in Sources */ = {isa = PBXBuildFile; fileRef = 1A0C4469219A4BC300F2AC00 /* MBIMBridge.h */; };
CDF6233A219A8A5600690038 /* MBIMBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A0C446A219A4BC300F2AC00 /* MBIMBridge.m */; };
@@ -130,7 +148,7 @@
1AAB32AF21F82EB7004A2A72 /* MBIMLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMLogging.h; sourceTree = "<group>"; };
1AAB32B021F82EB7004A2A72 /* MBIMLogging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMLogging.m; sourceTree = "<group>"; };
1AAB32B221F835BD004A2A72 /* KPServer.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KPServer.pch; sourceTree = "<group>"; };
1ACFCDE2219EB28A00E2C237 /* libCocoaHTTPServer.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCocoaHTTPServer.a; sourceTree = BUILT_PRODUCTS_DIR; };
1ACFCDE2219EB28A00E2C237 /* CocoaHTTPServer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CocoaHTTPServer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1ACFCDFC219EB2AB00E2C237 /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = "<group>"; };
1ACFCDFE219EB2AB00E2C237 /* README.markdown */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.markdown; sourceTree = "<group>"; };
1ACFCE00219EB2AB00E2C237 /* HTTPConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPConnection.h; sourceTree = "<group>"; };
@@ -203,17 +221,10 @@
CD14F1A9219FF3B800E7DD22 /* MBIMUpdateQueue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMUpdateQueue.m; sourceTree = "<group>"; };
CD14F1AB219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMConcurrentHTTPServer.h; sourceTree = "<group>"; };
CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMConcurrentHTTPServer.m; sourceTree = "<group>"; };
CD2782BB29527FE500C0C030 /* IMSharedUtilities.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IMSharedUtilities.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.Internal.sdk/System/Library/PrivateFrameworks/IMSharedUtilities.framework; sourceTree = DEVELOPER_DIR; };
CD2782BD2952832B00C0C030 /* MBIMImageUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMImageUtils.h; sourceTree = "<group>"; };
CD2782BE2952832B00C0C030 /* MBIMImageUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMImageUtils.m; sourceTree = "<group>"; };
CD2782FD2952875F00C0C030 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
CD2782FF2952876700C0C030 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; };
CD2ECEC0269539100055E302 /* MBIMAuthenticateOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAuthenticateOperation.h; sourceTree = "<group>"; };
CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAuthenticateOperation.m; sourceTree = "<group>"; };
CD2ECEC326953F2A0055E302 /* MBIMAuthToken.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAuthToken.h; sourceTree = "<group>"; };
CD2ECEC426953F2A0055E302 /* MBIMAuthToken.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAuthToken.m; sourceTree = "<group>"; };
CD3F62AF297769F2004305D9 /* MBIMURLUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMURLUtilities.h; sourceTree = "<group>"; };
CD3F62B0297769F2004305D9 /* MBIMURLUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMURLUtilities.m; sourceTree = "<group>"; };
CD602054219B5DFD0024D9C5 /* MBIMBridgeOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMBridgeOperation.h; sourceTree = "<group>"; };
CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMBridgeOperation.m; sourceTree = "<group>"; };
CD60205A219B623F0024D9C5 /* MBIMMessagesListOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMMessagesListOperation.h; sourceTree = "<group>"; };
@@ -227,17 +238,9 @@
CD83E161219BE91500F4CCEA /* libagentHook.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libagentHook.dylib; sourceTree = BUILT_PRODUCTS_DIR; };
CD83E165219BE91600F4CCEA /* agentHook.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = agentHook.m; sourceTree = "<group>"; };
CD83E1B5219BF78E00F4CCEA /* hookAgent.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = hookAgent.sh; sourceTree = "<group>"; };
CD936A2F289B31740093A1AC /* kordophoned-RestrictedEntitlements.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "kordophoned-RestrictedEntitlements.plist"; sourceTree = "<group>"; };
CD936A30289B353F0093A1AC /* MBIMErrorResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMErrorResponse.h; sourceTree = "<group>"; };
CD936A31289B353F0093A1AC /* MBIMErrorResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMErrorResponse.m; sourceTree = "<group>"; };
CD936A33289B47D50093A1AC /* MBIMVersionOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMVersionOperation.h; sourceTree = "<group>"; };
CD936A34289B47D50093A1AC /* MBIMVersionOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMVersionOperation.m; sourceTree = "<group>"; };
CD936A37289B49FC0093A1AC /* MBIMStatusOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMStatusOperation.h; sourceTree = "<group>"; };
CD936A38289B49FC0093A1AC /* MBIMStatusOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMStatusOperation.m; sourceTree = "<group>"; };
CDA64B452DFCBF3000E9B07E /* MBIMPingPongWebSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMPingPongWebSocket.h; sourceTree = "<group>"; };
CDA64B462DFCBF3000E9B07E /* MBIMPingPongWebSocket.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMPingPongWebSocket.m; sourceTree = "<group>"; };
CDDCF78B283F398C0087ABDF /* MBIMDeleteConversationOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMDeleteConversationOperation.h; sourceTree = "<group>"; };
CDDCF78C283F398C0087ABDF /* MBIMDeleteConversationOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMDeleteConversationOperation.m; sourceTree = "<group>"; };
CDD8C98226977D2A00551AE5 /* MBIMAliasValidationOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMAliasValidationOperation.h; sourceTree = "<group>"; };
CDD8C98326977D2A00551AE5 /* MBIMAliasValidationOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMAliasValidationOperation.m; sourceTree = "<group>"; };
CDD8C9852697996700551AE5 /* IDS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IDS.framework; path = System/Library/PrivateFrameworks/IDS.framework; sourceTree = SDKROOT; };
CDE4556221A3578A0041F5DD /* IMChat+Encoded.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "IMChat+Encoded.h"; sourceTree = "<group>"; };
CDE4556321A3578A0041F5DD /* IMChat+Encoded.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "IMChat+Encoded.m"; sourceTree = "<group>"; };
CDE4559F21A365AD0041F5DD /* MBIMMarkOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMMarkOperation.h; sourceTree = "<group>"; };
@@ -246,8 +249,6 @@
CDE455A321A5308D0041F5DD /* MBIMFetchAttachmentOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMFetchAttachmentOperation.m; sourceTree = "<group>"; };
CDE455A521A531ED0041F5DD /* MBIMDataResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMDataResponse.h; sourceTree = "<group>"; };
CDE455A621A531ED0041F5DD /* MBIMDataResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMDataResponse.m; sourceTree = "<group>"; };
CDEFF9FC2CACC7A700063C52 /* MBIMResolveHandleOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MBIMResolveHandleOperation.h; sourceTree = "<group>"; };
CDEFF9FD2CACC7A700063C52 /* MBIMResolveHandleOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MBIMResolveHandleOperation.m; sourceTree = "<group>"; };
CDF62332219A895D00690038 /* kordophoned */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = kordophoned; sourceTree = BUILT_PRODUCTS_DIR; };
CDF62334219A895D00690038 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -278,11 +279,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CD2782BC29527FE500C0C030 /* IMSharedUtilities.framework in Frameworks */,
CDD8C9862697996800551AE5 /* IDS.framework in Frameworks */,
1A257CCB23A8681200A4A2C8 /* Security.framework in Frameworks */,
CD2783002952876700C0C030 /* ImageIO.framework in Frameworks */,
1ACFCFDF219EB31400E2C237 /* libCocoaHTTPServer.a in Frameworks */,
CD2782FE2952875F00C0C030 /* CoreGraphics.framework in Frameworks */,
1ACFCFDF219EB31400E2C237 /* CocoaHTTPServer.framework in Frameworks */,
1A257CC923A867EF00A4A2C8 /* IMCore.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -310,7 +309,7 @@
CDF62332219A895D00690038 /* kordophoned */,
CD83E161219BE91500F4CCEA /* libagentHook.dylib */,
CD14F18B219E2DB400E7DD22 /* Tests.xctest */,
1ACFCDE2219EB28A00E2C237 /* libCocoaHTTPServer.a */,
1ACFCDE2219EB28A00E2C237 /* CocoaHTTPServer.framework */,
);
name = Products;
sourceTree = "<group>";
@@ -328,9 +327,7 @@
1A0C445E219A45B400F2AC00 /* Frameworks */ = {
isa = PBXGroup;
children = (
CD2782FF2952876700C0C030 /* ImageIO.framework */,
CD2782FD2952875F00C0C030 /* CoreGraphics.framework */,
CD2782BB29527FE500C0C030 /* IMSharedUtilities.framework */,
CDD8C9852697996700551AE5 /* IDS.framework */,
1A257CCA23A8681200A4A2C8 /* Security.framework */,
1A257CC823A867EF00A4A2C8 /* IMCore.framework */,
);
@@ -352,8 +349,6 @@
CD14F1AC219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m */,
1ACFCFE2219EB45300E2C237 /* MBIMHTTPConnection.h */,
1ACFCFE3219EB45300E2C237 /* MBIMHTTPConnection.m */,
CDA64B452DFCBF3000E9B07E /* MBIMPingPongWebSocket.h */,
CDA64B462DFCBF3000E9B07E /* MBIMPingPongWebSocket.m */,
);
path = Bridge;
sourceTree = "<group>";
@@ -365,14 +360,8 @@
1AA43E8E219EBB2D00EDF1A7 /* MBIMJSONDataResponse.m */,
CDE455A521A531ED0041F5DD /* MBIMDataResponse.h */,
CDE455A621A531ED0041F5DD /* MBIMDataResponse.m */,
CD936A30289B353F0093A1AC /* MBIMErrorResponse.h */,
CD936A31289B353F0093A1AC /* MBIMErrorResponse.m */,
1AA43E93219EC38E00EDF1A7 /* MBIMHTTPUtilities.h */,
1AA43E94219EC38E00EDF1A7 /* MBIMHTTPUtilities.m */,
CD2782BD2952832B00C0C030 /* MBIMImageUtils.h */,
CD2782BE2952832B00C0C030 /* MBIMImageUtils.m */,
CD3F62AF297769F2004305D9 /* MBIMURLUtilities.h */,
CD3F62B0297769F2004305D9 /* MBIMURLUtilities.m */,
);
path = Utilities;
sourceTree = "<group>";
@@ -538,32 +527,26 @@
isa = PBXGroup;
children = (
1AA43E90219EBB3400EDF1A7 /* Utilities */,
CD2ECEC0269539100055E302 /* MBIMAuthenticateOperation.h */,
CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */,
CD602054219B5DFD0024D9C5 /* MBIMBridgeOperation.h */,
CD602055219B5DFD0024D9C5 /* MBIMBridgeOperation.m */,
CDD8C98226977D2A00551AE5 /* MBIMAliasValidationOperation.h */,
CDD8C98326977D2A00551AE5 /* MBIMAliasValidationOperation.m */,
CD2ECEC0269539100055E302 /* MBIMAuthenticateOperation.h */,
CD2ECEC1269539100055E302 /* MBIMAuthenticateOperation.m */,
CD60205A219B623F0024D9C5 /* MBIMMessagesListOperation.h */,
CD60205B219B623F0024D9C5 /* MBIMMessagesListOperation.m */,
CD60205D219B674B0024D9C5 /* MBIMConversationListOperation.h */,
CD60205E219B674B0024D9C5 /* MBIMConversationListOperation.m */,
CDDCF78B283F398C0087ABDF /* MBIMDeleteConversationOperation.h */,
CDDCF78C283F398C0087ABDF /* MBIMDeleteConversationOperation.m */,
CDE455A221A5308D0041F5DD /* MBIMFetchAttachmentOperation.h */,
CDE455A321A5308D0041F5DD /* MBIMFetchAttachmentOperation.m */,
CDE4559F21A365AD0041F5DD /* MBIMMarkOperation.h */,
CDE455A021A365AD0041F5DD /* MBIMMarkOperation.m */,
CD60205A219B623F0024D9C5 /* MBIMMessagesListOperation.h */,
CD60205B219B623F0024D9C5 /* MBIMMessagesListOperation.m */,
CDEFF9FC2CACC7A700063C52 /* MBIMResolveHandleOperation.h */,
CDEFF9FD2CACC7A700063C52 /* MBIMResolveHandleOperation.m */,
CD602060219B68950024D9C5 /* MBIMSendMessageOperation.h */,
CD602061219B68950024D9C5 /* MBIMSendMessageOperation.m */,
CD936A37289B49FC0093A1AC /* MBIMStatusOperation.h */,
CD936A38289B49FC0093A1AC /* MBIMStatusOperation.m */,
CD14F19F219FE7D600E7DD22 /* MBIMUpdatePollOperation.h */,
CD14F1A0219FE7D600E7DD22 /* MBIMUpdatePollOperation.m */,
1AD8936C21EFD986009B599A /* MBIMUploadAttachmentOperation.h */,
1AD8936D21EFD986009B599A /* MBIMUploadAttachmentOperation.m */,
CD936A33289B47D50093A1AC /* MBIMVersionOperation.h */,
CD936A34289B47D50093A1AC /* MBIMVersionOperation.m */,
);
path = Operations;
sourceTree = "<group>";
@@ -595,7 +578,6 @@
1A0C446D219A4BCD00F2AC00 /* Bridge */,
CDF62334219A895D00690038 /* main.m */,
1AAB32B221F835BD004A2A72 /* KPServer.pch */,
CD936A2F289B31740093A1AC /* kordophoned-RestrictedEntitlements.plist */,
);
path = kordophone;
sourceTree = "<group>";
@@ -603,6 +585,40 @@
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
1ACFCDDF219EB28A00E2C237 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
1ACFCFCB219EB2AC00E2C237 /* DDLog.h in Headers */,
1ACFCF40219EB2AC00E2C237 /* MultipartFormDataParser.h in Headers */,
1ACFCF2F219EB2AC00E2C237 /* HTTPAsyncFileResponse.h in Headers */,
1ACFCF37219EB2AC00E2C237 /* HTTPDataResponse.h in Headers */,
1ACFCF45219EB2AC00E2C237 /* MultipartMessageHeaderField.h in Headers */,
1ACFCF3A219EB2AC00E2C237 /* HTTPDynamicFileResponse.h in Headers */,
1ACFCFCC219EB2AC00E2C237 /* DDAbstractDatabaseLogger.h in Headers */,
1ACFCFD6219EB2AC00E2C237 /* DDTTYLogger.h in Headers */,
1ACFCF4C219EB2AC00E2C237 /* DDData.h in Headers */,
1ACFCF2D219EB2AC00E2C237 /* WebSocket.h in Headers */,
1ACFCF49219EB2AC00E2C237 /* DDRange.h in Headers */,
1ACFCF32219EB2AC00E2C237 /* HTTPRedirectResponse.h in Headers */,
1ACFCF2E219EB2AC00E2C237 /* HTTPAuthenticationRequest.h in Headers */,
1ACFCFDC219EB2AC00E2C237 /* GCDAsyncSocket.h in Headers */,
1ACFCF4A219EB2AC00E2C237 /* DDNumber.h in Headers */,
1ACFCF3F219EB2AC00E2C237 /* HTTPResponse.h in Headers */,
1ACFCF39219EB2AC00E2C237 /* HTTPFileResponse.h in Headers */,
1ACFCFCF219EB2AC00E2C237 /* ContextFilterLogFormatter.h in Headers */,
1ACFCF3B219EB2AC00E2C237 /* HTTPServer.h in Headers */,
1ACFCF41219EB2AC00E2C237 /* MultipartMessageHeader.h in Headers */,
1ACFCF38219EB2AC00E2C237 /* HTTPErrorResponse.h in Headers */,
1ACFCF2B219EB2AC00E2C237 /* HTTPLogging.h in Headers */,
1ACFCFD0219EB2AC00E2C237 /* DispatchQueueLogFormatter.h in Headers */,
1ACFCF2C219EB2AC00E2C237 /* HTTPMessage.h in Headers */,
1ACFCFD9219EB2AC00E2C237 /* DDASLLogger.h in Headers */,
1ACFCFCE219EB2AC00E2C237 /* DDFileLogger.h in Headers */,
1ACFCF2A219EB2AC00E2C237 /* HTTPConnection.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
CD83E15D219BE91500F4CCEA /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
@@ -619,6 +635,8 @@
buildPhases = (
1ACFCDDD219EB28A00E2C237 /* Sources */,
1ACFCDDE219EB28A00E2C237 /* Frameworks */,
1ACFCDDF219EB28A00E2C237 /* Headers */,
1ACFCDE0219EB28A00E2C237 /* Resources */,
);
buildRules = (
);
@@ -626,8 +644,8 @@
);
name = CocoaHTTPServer;
productName = CocoaHTTPServer;
productReference = 1ACFCDE2219EB28A00E2C237 /* libCocoaHTTPServer.a */;
productType = "com.apple.product-type.library.static";
productReference = 1ACFCDE2219EB28A00E2C237 /* CocoaHTTPServer.framework */;
productType = "com.apple.product-type.framework";
};
CD14F18A219E2DB400E7DD22 /* Tests */ = {
isa = PBXNativeTarget;
@@ -668,10 +686,10 @@
isa = PBXNativeTarget;
buildConfigurationList = CDF62336219A895D00690038 /* Build configuration list for PBXNativeTarget "kordophoned" */;
buildPhases = (
CD936A36289B48930093A1AC /* Compile Version String */,
CDF6232E219A895D00690038 /* Sources */,
CDF6232F219A895D00690038 /* Frameworks */,
CDF62330219A895D00690038 /* CopyFiles */,
1AAB32AD21F8245D004A2A72 /* Add current directory to rpath */,
);
buildRules = (
);
@@ -729,6 +747,13 @@
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
1ACFCDE0219EB28A00E2C237 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
CD14F189219E2DB400E7DD22 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -739,7 +764,7 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
CD936A36289B48930093A1AC /* Compile Version String */ = {
1AAB32AD21F8245D004A2A72 /* Add current directory to rpath */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -748,14 +773,14 @@
);
inputPaths = (
);
name = "Compile Version String";
name = "Add current directory to rpath";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nOUTPUT_FILE=\"$DERIVED_FILE_DIR/MBIMVersion.c\"\nVERSION_STR=`git describe`\n\necho \"const char* MBIMVersion() { return \\\"$VERSION_STR\\\"; }\" > $OUTPUT_FILE\n";
shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\nRESULT=$(install_name_tool -delete_rpath $BUILT_PRODUCTS_DIR $BUILT_PRODUCTS_DIR/$EXECUTABLE_NAME 2> /dev/null)\n\ninstall_name_tool -add_rpath $BUILT_PRODUCTS_DIR $BUILT_PRODUCTS_DIR/$EXECUTABLE_NAME\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -812,7 +837,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CDA64B472DFCBF3000E9B07E /* MBIMPingPongWebSocket.m in Sources */,
CD14F1AD219FFAE100E7DD22 /* MBIMConcurrentHTTPServer.m in Sources */,
1AA43E91219EBC2C00EDF1A7 /* MBIMHTTPConnection.m in Sources */,
CDF62339219A8A5600690038 /* MBIMBridge.h in Sources */,
@@ -821,27 +845,21 @@
CD2ECEC526953F2A0055E302 /* MBIMAuthToken.m in Sources */,
CD83E156219BE10A00F4CCEA /* hooking.m in Sources */,
1AAB32B121F82EB7004A2A72 /* MBIMLogging.m in Sources */,
CDDCF78D283F398C0087ABDF /* MBIMDeleteConversationOperation.m in Sources */,
1AD8936E21EFD986009B599A /* MBIMUploadAttachmentOperation.m in Sources */,
CDF6233A219A8A5600690038 /* MBIMBridge.m in Sources */,
CDF62335219A895D00690038 /* main.m in Sources */,
CD60205C219B623F0024D9C5 /* MBIMMessagesListOperation.m in Sources */,
CDD8C98426977D2A00551AE5 /* MBIMAliasValidationOperation.m in Sources */,
CD14F1AA219FF3B800E7DD22 /* MBIMUpdateQueue.m in Sources */,
CD3F62B1297769F2004305D9 /* MBIMURLUtilities.m in Sources */,
CD14F1A4219FF22700E7DD22 /* IMMessageItem+Encoded.m in Sources */,
CD602062219B68950024D9C5 /* MBIMSendMessageOperation.m in Sources */,
CD14F1A1219FE7D600E7DD22 /* MBIMUpdatePollOperation.m in Sources */,
CD936A39289B49FC0093A1AC /* MBIMStatusOperation.m in Sources */,
CDEFF9FE2CACC7A700063C52 /* MBIMResolveHandleOperation.m in Sources */,
CDE455A121A365AD0041F5DD /* MBIMMarkOperation.m in Sources */,
CDE455A721A531ED0041F5DD /* MBIMDataResponse.m in Sources */,
CD936A35289B47D60093A1AC /* MBIMVersionOperation.m in Sources */,
CD602056219B5DFD0024D9C5 /* MBIMBridgeOperation.m in Sources */,
CD60205F219B674B0024D9C5 /* MBIMConversationListOperation.m in Sources */,
CDE4556421A3578A0041F5DD /* IMChat+Encoded.m in Sources */,
CD2782BF2952832B00C0C030 /* MBIMImageUtils.m in Sources */,
1AA43E8F219EBB2D00EDF1A7 /* MBIMJSONDataResponse.m in Sources */,
CD936A32289B353F0093A1AC /* MBIMErrorResponse.m in Sources */,
CD2ECEC2269539100055E302 /* MBIMAuthenticateOperation.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -987,10 +1005,27 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_VERSION = A;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.github.CocoaHTTPServer;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Debug;
};
@@ -998,9 +1033,26 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
FRAMEWORK_VERSION = A;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.CocoaHTTPServer;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
name = Release;
};
@@ -1076,11 +1128,9 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = NO;
CODE_SIGN_ENTITLEMENTS = "kordophone/kordophoned-RestrictedEntitlements.plist";
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
GCC_PREFIX_HEADER = kordophone/KPServer.pch;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SYSTEM_FRAMEWORK_SEARCH_PATHS = (
@@ -1097,7 +1147,6 @@
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic;
GCC_PREFIX_HEADER = kordophone/KPServer.pch;
OTHER_LDFLAGS = "-ObjC";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SYSTEM_FRAMEWORK_SEARCH_PATHS = (

View File

@@ -1,66 +1,53 @@
# Kordophone Monorepo
# Entitlements
Kordophone is an iMessage bridge: a lightweight server runs on a Mac and exposes an HTTP API so nonApple devices can send/receive iMessages. A set of clients (Android, Linux GTK, macOS Cocoa) talk to that API. A shared Rust library powers most clients, and a mock server helps local testing.
> Important: Interfacing with iMessage on macOS involves private APIs and restricted entitlements. See `server/README.md` for details and safety notes.
## Repository Layout
Toplevel projects in this monorepo:
- `server/` — macOS daemon that bridges to iMessage and exposes an HTTP REST API. Written in ObjectiveC/Cocoa. See `server/README.md`.
- `core/` — Rust workspace with the shared Kordophone client library and related tooling. Used by `gtk/` and `osx/`.
- `gtk/` — GTK4/Libadwaita Linux desktop client built on the Rust `core` library.
- `osx/` — macOS Cocoa client that uses the Rust `core` library and talks to the local Kordophone client daemon via XPC.
- `android/` — Android client. Currently implements its own API client (does not use the Rust library yet).
- `mock/` — Gobased mock server that emulates a Mac running the Kordophone server, for local development and tests.
Quick links:
- Server (mac daemon): `server/`
- Core (Rust workspace): `core/`
- GTK client (Linux): `gtk/`
- macOS client (Cocoa): `osx/`
- Android client: `android/`
- Mock server (Go): `mock/`
## How It Works
1. The macOS Kordophone Server (`server/`) runs on a Mac with iMessage and exposes an HTTP/JSON API and a WebSocket/updates channel.
2. Clients connect to that server to list conversations, fetch/send messages, upload/download attachments, and receive live updates.
3. A Rust library in `core/kordophone` implements the wire protocol and client behaviors. Linux/macOS clients use this library directly; Android currently ships a native Kotlin client.
4. The Rust client daemon (`core/kordophoned`) provides local caching and IPC (DBus on Linux, XPC on macOS) for GUI apps.
5. The `mock/` server simulates the server API for local development without a Mac.
## Getting Started
You can try the clients quickly against the mock server.
1) Run the mock server (no auth):
```bash
cd mock
go run ./... # or: make; ./kordophone-mock
You might to enable this default to use private entitlements
```
sudo defaults write /Library/Preferences/com.apple.security.coderequirements Entitlements -string always
```
The mock server listens on `http://localhost:5738`.
Maybe a better thing to do is to DYLD_PRELOAD `imagent` and swizzle `IMDAuditTokenTaskHasEntitlement` to always return YES.
2) Point a client at the mock server:
- Android: open Settings in the app and set the server host to `http://10.0.2.2:5738` (Android emulator) or `http://<your-host-ip>:5738` if running on device. Disable auth unless you started the mock with `--auth`.
- GTK (Linux): build and run the GTK app from `gtk/` (see its README) and configure it to use the local daemon backed by the server URL.
- macOS (Cocoa): build the app in `osx/` (see `osx/README.md`).
## Building/linking
If you get dyld errors running from the command line, use `install_name_tool` to update the @rpath (where @rpath points to where linked Frameworks like GCDWebServer is).
`install_name_tool -add_rpath . ./kordophoned`
To use a real Mac server instead, set your clients server URL to the host running `server/` with the correct scheme/port and authentication.
## Building
## Running
You need to hook imagent first to bypass entitlements check. Look at `hookAgent.sh`
Below are brief notes. Each subprojects README has more detail.
- Server (macOS): open `server/MessagesBridge.xcodeproj` in Xcode. See `server/README.md` for entitlements, SSL, and running.
- Core (Rust): install Rust (stable) and run `cargo build` inside `core/`. See `core/README.md`.
- GTK (Linux): see `gtk/README.md` for RPM build via `rpmbuild -ba dist/rpm/kordophone.spec`.
- macOS (Cocoa): open `osx/kordophone2.xcodeproj` in Xcode. See `osx/README.md`.
- Android: open `android/` in Android Studio and build. See `android/README.md` for configuration.
- Mock server (Go): `cd mock && go run ./...` or `make`.
## SSL
If you want to run with SSL, you have to generate a self-signed certificate, and have the Mac trust the root cert.
### Generate a root cert
1. Generate root key
`openssl genrsa -out Kordophone-root.key 4096`
2. Generate root certificate
`openssl req -x509 -new -nodes -key Kordophone-root.key -sha256 -days 1024 -out Kordophone-root.crt`
3. Add this certificate to the Mac's trust store via Keychain Access. Set to "Always Trust"
### Create signing certificate by signing a new cert with the root cert
1. Generate signing key
`openssl genrsa -out kp.localhost.key 2048`
2. Create certificate signing request
`openssl req -new -key kp.localhost.key -out kp.localhost.csr`
3. Sign the cert with the root cert
`openssl x509 -req -in kp.localhost.csr -CA Kordophone-root.crt -CAkey Kordophone-root.key -CAcreateserial -out kp.localhost.crt -days 365 -sha256`
4. kordophoned works with a signing cert in PKCS12 format. Convert the cert and the privkey to PKCS12
`openssl pkcs12 -export -in kp.localhost.crt -inkey kp.localhost.key -out certificate.p12 -name "Kordophone"`
### Start kordophone with the SSL options and provide the p12
`kordophoned -s -c certificate.p12`
## Authentication
Basic Authentication is also optional, but requires SSL to be enabled as well. To configure basic authentication, create a file containing your username and password on two separate lines encrypted with your GPG key.
`echo "username\npassword" > password.txt"`
`gpg -e -r (your email) -o password.asc password.txt`
Then run kordophoned with the following option
`kordophone -s -c certificate.p12 -a password.asc`
You may need to unlock your GPG keyring (via gpg-agent) when running kordophoned the first time.

View File

@@ -1,32 +0,0 @@
image: ubuntu/jammy
packages:
- openjdk-18-jdk
- gradle
- maven
sources:
- https://git.sr.ht/~buzzert/KordophoneDroid
secrets:
- a24d65d9-3e71-40e9-946d-0e9b73efacee # ~/.gradle/gradle.properties: contains keystore passwords
- 4fbe9d83-5f38-49c0-b93d-863d15e92a60 # ~/keystore.jks: Android keystore
tasks:
- setup: |
wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
unzip commandlinetools-linux-11076708_latest.zip
mkdir android-sdk
yes | ./cmdline-tools/bin/sdkmanager --sdk_root=android-sdk --licenses
./cmdline-tools/bin/sdkmanager --sdk_root=android-sdk "build-tools;34.0.0" "platforms;android-33"
- build: |
export ANDROID_HOME=~/android-sdk
cd KordophoneDroid/
./gradlew assembleRelease
- prepare: |
cd KordophoneDroid/app/build/outputs/apk/release/
cp app-arm64-v8a-release.apk ~/kordophone-arm64-v8a-release.apk
cp app-armeabi-v7a-release.apk ~/kordophone-armeabi-v7a-release.apk
cp app-x86_64-release.apk ~/kordophone-x86_64-release.apk
cp app-x86-release.apk ~/kordophone-x86-release.apk
artifacts:
- kordophone-arm64-v8a-release.apk
- kordophone-armeabi-v7a-release.apk
- kordophone-x86_64-release.apk
- kordophone-x86-release.apk

17
android/.gitignore vendored
View File

@@ -1,17 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
.idea/

3
android/.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
</component>
</project>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/backend" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,41 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.22" />
</component>
</project>

View File

@@ -1,9 +0,0 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

7
android/.idea/vcs.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,46 +0,0 @@
# Kordophone Android Client
Android client for the Kordophone iMessage bridge. This app connects to a running Kordophone server over HTTP/JSON and streams updates.
Note: This client currently implements its own API layer in Kotlin and does not yet use the shared Rust `core` library.
## Build & Run
Requirements:
- Android Studio (AGP 8.x)
- JDK 8+ toolchain (project uses Java 8 bytecode)
Steps:
1. Open `android/` in Android Studio.
2. Sync Gradle and build the project.
3. Run the `app` configuration on an emulator or device.
The app targets `minSdk 30`, `targetSdk 33` and uses Jetpack Compose.
## Configure Server
Set the server host and optional Basic Auth from the apps Settings screen.
- Server URL for local mock (emulator): `http://10.0.2.2:5738`
- Server URL for local mock (device): `http://<your-host-ip>:5738`
- Credentials: match the server if auth is enabled (mock supports `--auth`).
Settings are persisted using `EncryptedSharedPreferences`.
## Modules
- `app/` — UI (Compose), DI (Hilt), app wiring
- `backend/` — Kotlin API client, DB/cache (Realm), models, tests
## Testing with the Mock Server
Start the mock server in the repo root:
```bash
cd mock
go run ./... # or: make; ./kordophone-mock
```
Set the apps server URL to the mock address and run. The mock implements the Kordophone server API; see centralized API documentation (planned under `api/`).

View File

@@ -1,2 +0,0 @@
/build
/release

View File

@@ -1,155 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
namespace 'net.buzzert.kordophonedroid'
compileSdk 33
defaultConfig {
applicationId "net.buzzert.kordophonedroid"
minSdk 30
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
signingConfigs {
if (project.hasProperty('RELEASE_STORE_FILE')) {
release {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
if (project.hasProperty('RELEASE_STORE_FILE')) {
signingConfig signingConfigs.release
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildFeatures {
compose true
}
composeOptions {
// Note: this is strictly tied to a kotlin version, but isn't the version of kotlin exactly.
// See: https://developer.android.com/jetpack/androidx/releases/compose-kotlin
kotlinCompilerExtensionVersion '1.4.8'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
splits {
abi {
// Enable building for multiple ABIs
enable true
// Include x86/x86_64 APKs
include "x86", "x86_64"
universalApk false
}
}
buildToolsVersion '33.0.1'
}
kotlin {
jvmToolchain(8)
}
dependencies {
implementation "androidx.compose.material3:material3:1.1.1"
implementation "androidx.core:core-ktx:${kotlin_version}"
// Kordophone lib
implementation project(':backend')
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha06'
// Navigation
def nav_version = "2.6.0"
// Java language implementation
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// Feature module Support
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
// Testing Navigation
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
// Jetpack Compose Integration
implementation "androidx.navigation:navigation-compose:$nav_version"
// Jetpack Compose
def compose_version = "1.4.3"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.activity:activity-compose:$compose_version"
// Lifecycle
def lifecycle_version = "2.6.1"
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
// Hilt (dependency injection)
implementation "com.google.dagger:hilt-android:${hilt_version}"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"
kapt "com.google.dagger:hilt-compiler:${hilt_version}"
// Coil (image loading library)
implementation "io.coil-kt:coil:2.4.0"
implementation "io.coil-kt:coil-compose:2.4.0"
// Disk LRU Cache
implementation "com.jakewharton:disklrucache:2.0.2"
// Zooming in images
implementation "net.engawapg.lib:zoomable:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}
// Allow references to generated code
kapt {
correctErrorTypes true
}

View File

@@ -1,21 +0,0 @@
# 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

@@ -1,137 +0,0 @@
package previews
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import net.buzzert.kordophonedroid.R
import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListItem
import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListScreen
import net.buzzert.kordophonedroid.ui.conversationlist.NoContentView
import net.buzzert.kordophonedroid.ui.messagelist.AttachmentRowItem
import net.buzzert.kordophonedroid.ui.messagelist.MessageEntry
import net.buzzert.kordophonedroid.ui.messagelist.MessageListItem
import net.buzzert.kordophonedroid.ui.messagelist.MessageMetadata
import net.buzzert.kordophonedroid.ui.messagelist.MessageTranscript
import net.buzzert.kordophonedroid.ui.settings.SettingsScreen
import java.util.Date
// - Conversation List
@Preview
@Composable
fun ConversationListItemPreview() {
Column(modifier = Modifier.background(MaterialTheme.colors.background)) {
ConversationListItem(name = "James Magahern", id = "asdf", lastMessagePreview = "This is a test", date = Date(), isUnread = true) {}
}
}
@Preview
@Composable
fun ConversationListScreenPreview() {
ConversationListScreen()
}
// - Message List
private fun testMessageMetadata(fromMe: Boolean, delivered: Boolean): MessageMetadata {
return MessageMetadata(
fromMe = fromMe,
fromAddress = if (fromMe) "<me>" else "cool@cool.com",
date = Date(),
delivered = delivered,
)
}
private fun makeTestTextMessageItem(text: String, fromMe: Boolean, delivered: Boolean = true): MessageListItem {
return MessageListItem.TextMessage(
text = text,
metadata = testMessageMetadata(fromMe = fromMe, delivered = delivered)
)
}
private fun makeTestImageMessageItem(fromMe: Boolean, delivered: Boolean = true): MessageListItem {
return MessageListItem.ImageAttachmentMessage(
guid = "asdf",
metadata = testMessageMetadata(fromMe, delivered)
)
}
@Preview
@Composable
private fun MessageListScreenPreview() {
val messages = listOf<MessageListItem>(
makeTestImageMessageItem(false),
makeTestTextMessageItem("Hello", false),
makeTestTextMessageItem( "Hey there, this is a longer text message that might wrap to another line", true),
makeTestTextMessageItem("How's it going", fromMe = true, delivered = false)
).reversed()
Scaffold() {
MessageTranscript(
messages = messages,
paddingValues = it,
showSenders = true,
attachmentUris = setOf(),
onAddAttachment = {},
onClearAttachments = {},
onSendMessage = {}
)
}
}
@Preview(showBackground = true)
@Composable
private fun MessageEntryPreview() {
var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue("Hello this is some text that might wrap multiple lines to show that there must be some padding here. "))
}
MessageEntry(onSend = {}, onTextChanged = {}, textFieldValue = textState)
}
@Preview(showBackground = true)
@Composable
private fun MessageEntryWithAttachmentsPreview() {
var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue("Attachments"))
}
MessageEntry(onSend = {}, onTextChanged = {}, textFieldValue = textState, attachmentItems = listOf(
AttachmentRowItem(painterResource(id = R.drawable.sedona), "id")
))
}
// - No content
@Preview
@Composable
fun NoContentPreview() {
Scaffold {
NoContentView(
icon = R.drawable.storage,
text = "Server not configured",
onSettings = {},
modifier = Modifier.padding(it)
)
}
}
// - Settings
@Preview
@Composable
fun SettingsPreview() {
SettingsScreen()
}

View File

@@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.KordophoneDroid"
android:name=".KordophoneApplication"
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
>
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.KordophoneDroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<!-- Message List Deep Link -->
<data android:scheme="kordophone" android:host="messages" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<service
android:name=".UpdateMonitorService"
android:foregroundServiceType="dataSync"
android:exported="false" />
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,41 +0,0 @@
package net.buzzert.kordophonedroid
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import net.buzzert.kordophone.backend.db.CachedChatDatabase
import net.buzzert.kordophone.backend.server.APIClientFactory
import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophonedroid.ui.attachments.AttachmentImageLoader
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
fun provideChatRepository(configRepository: ServerConfigRepository): ChatRepository {
val serverConfig = configRepository.serverConfig.value
val server = serverConfig.serverName
val authentication = serverConfig.authentication?.toBackendAuthentication()
val client = APIClientFactory.createClient(server, authentication)
val database = CachedChatDatabase.liveDatabase()
return ChatRepository(client, database)
}
@Singleton
@Provides
fun provideAttachmentFactory(
chatRepository: ChatRepository,
@ApplicationContext context: Context
): AttachmentImageLoader
{
return AttachmentImageLoader(chatRepository, context)
}
}

View File

@@ -1,13 +0,0 @@
package net.buzzert.kordophonedroid
import android.app.Application
import androidx.hilt.navigation.compose.hiltViewModel
import dagger.hilt.android.HiltAndroidApp
import net.buzzert.kordophonedroid.data.AppContainer
@HiltAndroidApp
class KordophoneApplication : Application() {
override fun onCreate() {
super.onCreate()
}
}

View File

@@ -1,33 +0,0 @@
package net.buzzert.kordophonedroid
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.app.ActivityCompat
import dagger.hilt.android.AndroidEntryPoint
import net.buzzert.kordophonedroid.ui.KordophoneApp
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Ask for notifications
val hasPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
if (hasPermission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1234)
}
// Start update monitor service
val intent = Intent(this, UpdateMonitorService::class.java)
startService(intent)
setContent {
KordophoneApp()
}
}
}

View File

@@ -1,161 +0,0 @@
package net.buzzert.kordophonedroid
import android.Manifest
import android.R
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.IBinder
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri
import androidx.navigation.NavDeepLinkBuilder
import androidx.navigation.NavDeepLinkRequest
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import net.buzzert.kordophone.backend.model.Message
import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophonedroid.ui.Destination
import javax.inject.Inject
const val PUSH_CHANNEL_ID = "net.buzzert.kordophone.persistentNotification"
const val NEW_MESSAGE_CHANNEL_ID = "net.buzzert.kordophone.newMessage"
const val UPDATER_LOG = "UpdateService"
@AndroidEntryPoint
class UpdateMonitorService: Service()
{
@Inject lateinit var chatRepository: ChatRepository
private var newMessageID: Int = 0
private var watchJob: Job? = null
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private fun createNotificationChannel(channelId: String, channelName: String) {
val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT)
chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
chan.lightColor = Color.BLUE
val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
}
override fun onCreate() {
super.onCreate()
Log.v(UPDATER_LOG, "UpdateMonitor onCreate: Begin watching for updates.")
createNotificationChannel(NEW_MESSAGE_CHANNEL_ID, "New Messages")
// Connect to monitor
chatRepository.beginWatchingForUpdates(scope)
// Connect to new message flow for notifications
watchJob?.cancel()
watchJob = scope.launch {
chatRepository.newMessages.collectLatest(::onReceiveNewMessage)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel(PUSH_CHANNEL_ID, "Update Monitor Service")
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
)
val notification: Notification = NotificationCompat.Builder(this, PUSH_CHANNEL_ID)
.setContentTitle("Kordophone Connected")
.setContentText("Kordophone is listening for new messages.")
.setSmallIcon(R.drawable.sym_action_chat)
.setContentIntent(pendingIntent)
.setShowWhen(false)
.setSilent(true)
.setOngoing(true)
.build()
startForeground(5738, notification)
// Restart if we get killed
return START_STICKY
}
private fun onReceiveNewMessage(message: Message) {
val hasPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
if (hasPermission != PackageManager.PERMISSION_GRANTED) {
Log.e(UPDATER_LOG, "No permissions to post notifications.")
return
}
if (message.conversation.unreadCount == 0) {
// Not unread.
Log.v(UPDATER_LOG, "Ignoring read message.")
return
}
if (message.sender == null) {
// From me.
Log.v(UPDATER_LOG, "Ignoring message from me.")
return
}
if (message.conversation.isGroupChat) {
// For now, since these can be noisy and there's no UI for changing it, ignore group chats.
Log.v(UPDATER_LOG, "Ignoring group chat message.")
return
}
val guid = message.conversation.guid
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"kordophone://messages/$guid".toUri(),
this,
MainActivity::class.java
)
val pendingIntent = PendingIntent.getActivity(this, 0, deepLinkIntent, PendingIntent.FLAG_IMMUTABLE)
val groupId = message.conversation.guid
val notification = NotificationCompat.Builder(this, NEW_MESSAGE_CHANNEL_ID)
.setContentTitle(message.sender)
.setContentText(message.text)
.setSmallIcon(R.drawable.stat_notify_chat)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setGroup(groupId)
.build()
val manager = NotificationManagerCompat.from(this)
manager.notify(newMessageID++, notification)
}
override fun onBind(intent: Intent?): IBinder? {
// no binding
return null
}
override fun onDestroy() {
super.onDestroy()
chatRepository.stopWatchingForUpdates()
job.cancel()
}
}

View File

@@ -1,15 +0,0 @@
package net.buzzert.kordophonedroid.data
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import dagger.hilt.android.lifecycle.HiltViewModel
import net.buzzert.kordophone.backend.server.ChatRepository
import javax.inject.Inject
@HiltViewModel
class AppContainer @Inject constructor(
val repository: ChatRepository
) : ViewModel() {
}

View File

@@ -1,110 +0,0 @@
package net.buzzert.kordophonedroid.ui
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHost
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navDeepLink
import kotlinx.coroutines.flow.collectLatest
import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophonedroid.data.AppContainer
import net.buzzert.kordophonedroid.ui.attachments.AttachmentViewer
import net.buzzert.kordophonedroid.ui.theme.KordophoneTheme
import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListScreen
import net.buzzert.kordophonedroid.ui.messagelist.MessageListScreen
import net.buzzert.kordophonedroid.ui.settings.SettingsScreen
sealed class Destination(val route: String) {
object ConversationList : Destination("conversations")
object Settings : Destination("settings")
object MessageList : Destination("messages/{id}") {
fun createRoute(data: String) = "messages/$data"
}
object AttachmentViewer : Destination("attachment/{guid}") {
fun createRoute(guid: String) = "attachment/$guid"
}
}
val LocalNavController = compositionLocalOf<NavHostController> { error("No nav host") }
@Composable
fun ErrorDialog(title: String, body: String, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = { onDismiss() },
title = { Text(title) },
text = { Text(body) },
confirmButton = {
Button(onClick = { onDismiss() }) {
Text("OK")
}
}
)
}
@Composable
fun KordophoneApp(
appContainer: AppContainer = hiltViewModel(),
) {
KordophoneTheme {
val navController = rememberNavController()
val errorVisible = remember { mutableStateOf<ChatRepository.Error?>(null) }
val error = appContainer.repository.errorEncounteredChannel.collectAsStateWithLifecycle(
initialValue = null
)
LaunchedEffect(key1 = error.value) {
errorVisible.value = error.value
}
CompositionLocalProvider(LocalNavController provides navController) {
NavHost(
navController = navController,
startDestination = Destination.ConversationList.route,
) {
composable(Destination.ConversationList.route) {
ConversationListScreen()
}
composable(
route = Destination.MessageList.route,
deepLinks = listOf(navDeepLink { uriPattern = "kordophone://messages/{id}" })
) {
val conversationID = it.arguments?.getString("id")!!
MessageListScreen(conversationGUID = conversationID)
}
composable(Destination.Settings.route) {
SettingsScreen()
}
composable(Destination.AttachmentViewer.route) {
val guid = it.arguments?.getString("guid")!!
AttachmentViewer(attachmentGuid = guid)
}
}
}
errorVisible.value?.let {
ErrorDialog(title = it.title, body = it.description) {
errorVisible.value = null
}
}
}
}

View File

@@ -1,116 +0,0 @@
package net.buzzert.kordophonedroid.ui.attachments
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import coil.Coil
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.annotation.ExperimentalCoilApi
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.disk.DiskCache
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.request.Options
import dagger.hilt.android.qualifiers.ApplicationContext
import net.buzzert.kordophone.backend.server.ChatRepository
const val AVM_LOG: String = "AttachmentImageLoader"
data class AttachmentFetchData(
val guid: String,
val preview: Boolean = false
)
class AttachmentImageLoader(
private val repository: ChatRepository,
@ApplicationContext val application: Context,
) : ViewModel(), ImageLoaderFactory, Fetcher.Factory<AttachmentFetchData>
{
init {
// Register Coil image loader
Coil.setImageLoader(this)
}
override fun newImageLoader(): ImageLoader {
val factory = this
return ImageLoader.Builder(application)
.components {
// Adds the FetcherFactory
add(factory)
}
.build()
}
override fun create(
data: AttachmentFetchData,
options: Options,
imageLoader: ImageLoader
): Fetcher {
return AttachmentFetcher(repository, application, data)
}
}
private class AttachmentFetcher(
val repository: ChatRepository,
val context: Context,
val data: AttachmentFetchData
): Fetcher {
val cache = DiskCache.Builder()
.directory(context.cacheDir.resolve("attachments"))
.maxSizePercent(0.02)
.build()
val cacheKey: String get() { return data.guid + if (data.preview) "_preview" else "" }
@OptIn(ExperimentalCoilApi::class)
override suspend fun fetch(): FetchResult {
// Try loading from cache
var snapshot = cache.openSnapshot(cacheKey)
if (snapshot != null) {
Log.d(AVM_LOG, "Found attachment ${data.guid} in disk cache")
return SourceResult(
source = snapshot.toImageSource(),
dataSource = DataSource.DISK,
mimeType = null,
)
}
Log.d(AVM_LOG, "Loading attachment ${data.guid} from network")
val source = repository.fetchAttachmentDataSource(data.guid, data.preview)
// Save to cache
val editor = cache.openEditor(cacheKey)
if (editor != null) {
Log.d(AVM_LOG, "Writing attachment ${data.guid} to disk cache")
cache.fileSystem.write(editor.data) {
source.readAll(this)
}
snapshot = editor.commitAndOpenSnapshot()
if (snapshot != null) {
return SourceResult(
source = snapshot.toImageSource(),
dataSource = DataSource.NETWORK,
mimeType = null
)
}
}
// We'll go down this path if for some reason we couldn't save to cache.
return SourceResult(
source = ImageSource(source, context),
dataSource = DataSource.NETWORK,
mimeType = null,
)
}
@OptIn(ExperimentalCoilApi::class)
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
val fileSystem = cache.fileSystem
return ImageSource(data, fileSystem, cacheKey, this)
}
}

View File

@@ -1,70 +0,0 @@
package net.buzzert.kordophonedroid.ui.attachments
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import net.buzzert.kordophonedroid.ui.LocalNavController
import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
@Composable
fun AttachmentViewer(attachmentGuid: String) {
var topBarVisible = remember { mutableStateOf(true) }
val navController = LocalNavController.current
Scaffold(topBar = {
KordophoneTopAppBar(
title = "Attachment",
backAction = { navController.popBackStack() },
visible = topBarVisible.value
)
}) { padding ->
val zoomState = rememberZoomState()
val interactionSource = remember { MutableInteractionSource() }
val data = AttachmentFetchData(attachmentGuid, preview = false)
Column(modifier = Modifier.padding(padding)) {
Spacer(modifier = Modifier.weight(1f))
SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(data)
.crossfade(true)
.build(),
contentDescription = "",
loading = {
Box {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
},
modifier = Modifier
.zoomable(zoomState)
.fillMaxWidth()
.align(Alignment.CenterHorizontally)
.clickable(
interactionSource = interactionSource,
indication = null
) {
topBarVisible.value = !topBarVisible.value
}
)
Spacer(modifier = Modifier.weight(1f))
}
}
}

View File

@@ -1,248 +0,0 @@
package net.buzzert.kordophonedroid.ui.conversationlist
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophonedroid.R
import net.buzzert.kordophonedroid.ui.Destination
import net.buzzert.kordophonedroid.ui.LocalNavController
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
fun formatDateTime(dateTime: LocalDateTime): String {
val formatter: DateTimeFormatter = if (LocalDate.now().isEqual(dateTime.toLocalDate())) {
DateTimeFormatter.ofPattern("HH:mm") // show just the time
} else {
DateTimeFormatter.ofPattern("M/d/yy") // show day/month/year
}
return dateTime.format(formatter)
}
@Composable
fun ConversationListScreen(
viewModel: ConversationListViewModel = hiltViewModel(),
) {
val conversations by viewModel.conversations.collectAsStateWithLifecycle(initialValue = emptyList())
val encounteredError by viewModel.encounteredConnectionError
ConversationListView(
conversations = conversations,
isConfigured = viewModel.isServerConfigured,
encounteredError = encounteredError,
onRefresh = suspend { viewModel.refresh() }
)
}
@Composable
@OptIn(ExperimentalMaterialApi::class)
fun ConversationListView(
conversations: List<Conversation>,
isConfigured: Boolean = true,
encounteredError: Boolean = false,
onRefresh: suspend () -> Unit,
) {
val listState = rememberLazyListState()
val refreshScope = rememberCoroutineScope()
var refreshing by remember { mutableStateOf(false) }
fun refresh() = refreshScope.launch {
refreshing = true
onRefresh()
refreshing = false
}
val showErrorScreen = conversations.isEmpty() && encounteredError
val refreshState = rememberPullRefreshState(refreshing = refreshing, onRefresh = ::refresh)
val navController = LocalNavController.current
val onSettingsInvoked = { navController.navigate(Destination.Settings.route) }
Scaffold(
topBar = {
TopAppBar(title = { Text("Conversations") }, actions = {
IconButton(onClick = onSettingsInvoked) {
Icon(Icons.Rounded.Settings, contentDescription = "Settings")
}
})
}
) {
if (showErrorScreen) {
NoContentView(
icon = R.drawable.error,
text = "Connection error",
onSettings = onSettingsInvoked
)
} else if (!isConfigured) {
NoContentView(
icon = R.drawable.storage,
text = "Server not configured",
onSettings = onSettingsInvoked
)
} else {
Box(Modifier.pullRefresh(refreshState)) {
LazyColumn(
state = listState,
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
items(conversations) { conversation ->
val clickHandler = {
val route = Destination.MessageList.createRoute(conversation.guid)
navController.navigate(route)
}
ConversationListItem(
name = conversation.formattedDisplayName(),
id = conversation.guid,
isUnread = conversation.unreadCount > 0,
lastMessagePreview = conversation.lastMessagePreview ?: "",
date = conversation.date,
onClick = clickHandler
)
}
}
PullRefreshIndicator(
refreshing = refreshing,
state = refreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
}
}
@Composable
fun ConversationListItem(
name: String,
id: String,
isUnread: Boolean,
lastMessagePreview: String,
date: Date,
onClick: () -> Unit
) {
val unreadSize = 12.dp
val horizontalPadding = 8.dp
val verticalPadding = 14.dp
Row(
Modifier
.clickable(onClick = onClick)
) {
Spacer(Modifier.width(horizontalPadding))
// Unread icon
if (isUnread) {
UnreadIndicator(
size = unreadSize,
modifier = Modifier.align(Alignment.CenterVertically)
)
} else {
Spacer(modifier = Modifier.size(unreadSize))
}
Spacer(Modifier.width(horizontalPadding))
Column {
Spacer(Modifier.height(verticalPadding))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(
name,
style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.weight(1f, fill = true)
)
Spacer(modifier = Modifier)
Text(
formatDateTime(
date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
),
color = MaterialTheme.colors.onBackground.copy(alpha = 0.4f),
maxLines = 1,
modifier = Modifier
.align(Alignment.CenterVertically)
,
)
Spacer(Modifier.width(horizontalPadding))
}
Text(lastMessagePreview, maxLines = 1, overflow = TextOverflow.Ellipsis)
Spacer(Modifier.height(verticalPadding))
Divider()
}
}
}
@Composable
fun UnreadIndicator(size: Dp, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(size)
.background(
color = MaterialTheme.colors.primary,
shape = CircleShape
)
)
}

View File

@@ -1,74 +0,0 @@
package net.buzzert.kordophonedroid.ui.conversationlist
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophone.backend.server.APIClientFactory
import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
import javax.inject.Inject
const val CL_VM_LOG: String = "ConversationListViewModel"
@HiltViewModel
class ConversationListViewModel @Inject constructor(
private val chatRepository: ChatRepository,
private val serverConfigRepository: ServerConfigRepository,
) : ViewModel() {
val conversations: Flow<List<Conversation>>
get() = chatRepository.conversationChanges
.shareIn(viewModelScope, started = SharingStarted.WhileSubscribed())
.map {
it.sortedBy { it.date }
.reversed()
}
val isServerConfigured: Boolean
get() = chatRepository.isConfigured
val encounteredConnectionError: State<Boolean>
get() = _encounteredConnectionError
private val _encounteredConnectionError = mutableStateOf(false)
init {
// Watch for config changes
viewModelScope.launch {
serverConfigRepository.serverConfig.collect { config ->
Log.d(CL_VM_LOG, "Got settings change.")
// Make new APIClient
val baseURL = config.serverName
val authentication = config.authentication?.toBackendAuthentication()
val apiClient = APIClientFactory.createClient(baseURL, authentication)
chatRepository.updateAPIClient(apiClient)
// Perform db synchronization
withContext(Dispatchers.IO) {
chatRepository.synchronize()
}
}
}
viewModelScope.launch {
chatRepository.errorEncounteredChannel.collect {
_encounteredConnectionError.value = true
}
}
}
suspend fun refresh() {
chatRepository.synchronize()
}
}

View File

@@ -1,61 +0,0 @@
package net.buzzert.kordophonedroid.ui.conversationlist
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
@Composable
fun NoContentView(
@DrawableRes icon: Int,
text: String,
onSettings: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(icon),
"server icon",
modifier = Modifier
.height(150.dp)
.width(150.dp)
.alpha(0.5F)
)
Spacer(Modifier)
Text(
text = text,
fontSize = 5.0.em,
modifier = Modifier
.alpha(0.5F)
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onSettings) {
Text("Settings")
}
}
}

View File

@@ -1,166 +0,0 @@
package net.buzzert.kordophonedroid.ui.messagelist
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material3.ElevatedButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import net.buzzert.kordophonedroid.R
data class AttachmentRowItem(
val painter: Painter,
val id: String,
)
@Composable
fun AttachmentRow(
attachmentItems: List<AttachmentRowItem>,
onClear: () -> Unit,
) {
Divider()
Row(
modifier = Modifier
.height(120.dp)
.fillMaxWidth()
.background(MaterialTheme.colors.onSurface.copy(0.08f))
.padding(8.dp)
) {
LazyRow {
attachmentItems.forEach { attachmentItem ->
item {
Image(
painter = attachmentItem.painter,
contentDescription = "attachment",
contentScale = ContentScale.Crop,
modifier = Modifier
.aspectRatio(1.0f)
.clip(RoundedCornerShape(4.dp))
)
Spacer(Modifier.width(4.dp))
}
}
}
Spacer(Modifier.weight(1f))
ElevatedButton(
onClick = onClear,
colors = ButtonDefaults.elevatedButtonColors(
containerColor = MaterialTheme.colors.background
),
modifier = Modifier.align(Alignment.CenterVertically)
) {
Text("Remove")
}
}
}
@Composable
fun MessageEntry(
textFieldValue: TextFieldValue,
attachmentItems: List<AttachmentRowItem> = listOf(),
onAddAttachment: () -> Unit = {},
onClearAttachments: () -> Unit = {},
onTextChanged: (TextFieldValue) -> Unit,
onSend: () -> Unit,
) {
Column {
if (attachmentItems.isNotEmpty()) {
AttachmentRow(attachmentItems, onClear = onClearAttachments)
}
Row(
modifier = Modifier
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.18f))
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 4.dp)
.imePadding()
.navigationBarsPadding()
) {
IconButton(
onClick = onAddAttachment,
) {
Icon(
painter = painterResource(id = R.drawable.attach_file),
contentDescription = "Attach File"
)
}
Spacer(Modifier.width(8.dp))
Surface(
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
.shadow(3.dp)
) {
BasicTextField(
value = textFieldValue,
onValueChange = { onTextChanged(it) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
cursorBrush = SolidColor(MaterialTheme.colors.onBackground),
textStyle = MaterialTheme.typography.body1.copy(MaterialTheme.colors.onBackground),
decorationBox = { textContent ->
if (textFieldValue.text.isEmpty()) {
Text(
text = "Message",
style = MaterialTheme.typography.body1.copy(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
)
)
}
textContent()
}
)
}
Spacer(Modifier.width(8.dp))
Button(
onClick = onSend,
enabled = (attachmentItems.isNotEmpty() || textFieldValue.text.isNotEmpty())
) {
Text(text = "Send")
}
Spacer(Modifier.width(8.dp))
}
}
}

View File

@@ -1,433 +0,0 @@
package net.buzzert.kordophonedroid.ui.messagelist
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationManagerCompat
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.SubcomposeAsyncImage
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
import net.buzzert.kordophone.backend.model.GUID
import net.buzzert.kordophonedroid.ui.Destination
import net.buzzert.kordophonedroid.ui.LocalNavController
import net.buzzert.kordophonedroid.ui.attachments.AttachmentFetchData
import net.buzzert.kordophonedroid.ui.shared.LINK_ANNOTATION_TAG
import net.buzzert.kordophonedroid.ui.shared.linkify
import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar
import java.text.SimpleDateFormat
import java.time.Duration
import java.util.Date
private val IncomingChatBubbleShape = RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp)
private val OutgoingChatBubbleShape = RoundedCornerShape(20.dp, 4.dp, 20.dp, 20.dp)
data class MessageMetadata(
val fromAddress: String,
val fromMe: Boolean,
val date: Date,
val delivered: Boolean = true,
)
interface MessageMetadataProvider {
val metadata: MessageMetadata
}
sealed class MessageListItem: MessageMetadataProvider {
data class TextMessage(val text: String, override val metadata: MessageMetadata): MessageListItem()
data class ImageAttachmentMessage(val guid: String, override val metadata: MessageMetadata): MessageListItem()
}
@Composable
fun MessageListScreen(
conversationGUID: GUID,
viewModel: MessageListViewModel = hiltViewModel(),
) {
viewModel.conversationGUID = conversationGUID
// Synchronize on launch
val context = LocalContext.current
LaunchedEffect(Unit) {
// Clear notifications for this conversation
with(NotificationManagerCompat.from(context)) {
// Not sure how to cancel individual notifications, or groups yet...
cancelAll()
}
viewModel.markAsRead()
viewModel.synchronize()
}
val messages by viewModel.messages.collectAsStateWithLifecycle(initialValue = listOf())
val messageItems = mutableListOf<MessageListItem>()
for (message in messages) {
val metadata = MessageMetadata(
fromMe = message.sender == null,
date = message.date,
fromAddress = message.sender ?: "<me>",
delivered = !viewModel.isPendingMessage(message)
)
// Collect attachments
message.attachmentGUIDs?.let { guids ->
guids.forEach { guid ->
val item = MessageListItem.ImageAttachmentMessage(
guid = guid,
metadata = metadata
)
messageItems.add(item)
}
}
val displayText = message.displayText.trim()
if (displayText.isNotEmpty()) {
val textMessage = MessageListItem.TextMessage(text = displayText, metadata = metadata)
messageItems.add(textMessage)
}
}
var attachmentUris by remember { mutableStateOf<Set<Uri>>(mutableSetOf()) }
val imagePicker = rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let {
attachmentUris = attachmentUris.plus(it)
}
}
val navController = LocalNavController.current
Scaffold(
topBar = {
KordophoneTopAppBar(title = viewModel.title, backAction = { navController.popBackStack() })
}) { padding ->
MessageTranscript(
messages = messageItems,
paddingValues = padding,
showSenders = viewModel.isGroupChat,
attachmentUris = attachmentUris,
onAddAttachment = {
imagePicker.launch("image/*")
},
onClearAttachments = {
attachmentUris = setOf()
},
onSendMessage = { text ->
viewModel.enqueueOutgoingMessage(
text = text,
attachmentUris = attachmentUris,
context = context
)
// Clear pending attachments
attachmentUris = setOf()
}
)
}
}
@Composable
fun MessageTranscript(
messages: List<MessageListItem>,
paddingValues: PaddingValues,
showSenders: Boolean,
attachmentUris: Set<Uri>,
onAddAttachment: () -> Unit,
onClearAttachments: () -> Unit,
onSendMessage: (text: String) -> Unit,
) {
val scrollState = rememberLazyListState()
var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue())
}
val attachmentRowItems = attachmentUris.map {
AttachmentRowItem(
painter = rememberAsyncImagePainter(model = it),
id = "attachmentID"
)
}
Column(
Modifier
.fillMaxSize()
.padding(paddingValues)) {
Messages(
messages = messages,
modifier = Modifier.weight(1f),
showSenders = showSenders,
scrollState = scrollState
)
MessageEntry(
onTextChanged = { textState = it },
textFieldValue = textState,
attachmentItems = attachmentRowItems,
onAddAttachment = onAddAttachment,
onClearAttachments = onClearAttachments,
onSend = {
onSendMessage(textState.text)
// Clear text state
textState = TextFieldValue()
},
)
}
}
@Composable
fun Messages(
messages: List<MessageListItem>,
showSenders: Boolean,
modifier: Modifier = Modifier,
scrollState: LazyListState
) {
Box(modifier = modifier) {
LazyColumn(
reverseLayout = true,
state = scrollState,
contentPadding = PaddingValues(vertical = 8.dp),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
val dateFormatter = SimpleDateFormat.getDateTimeInstance()
for (index in messages.indices) {
val content = messages[index]
var previousMessage: MessageListItem? = null
if ((index + 1) < messages.count()) {
previousMessage = messages[index + 1]
}
val duration: Duration? = if (previousMessage != null) Duration.between(
previousMessage.metadata.date.toInstant(),
content.metadata.date.toInstant()
) else null
val leapMessage = (
duration == null || (
duration.toMinutes() > 30
)
)
val repeatMessage = !leapMessage && (
previousMessage == null || (
(previousMessage.metadata.fromAddress == content.metadata.fromAddress)
)
)
// Remember: This is upside down.
item {
when (content) {
is MessageListItem.TextMessage -> {
MessageBubble(
text = content.text,
mine = content.metadata.fromMe,
modifier = Modifier
.alpha(if (!content.metadata.delivered) 0.5F else 1.0f)
)
}
is MessageListItem.ImageAttachmentMessage -> {
ImageBubble(guid = content.guid, mine = content.metadata.fromMe)
}
}
// Sender
if (!content.metadata.fromMe && showSenders && !repeatMessage) {
Text(
text = content.metadata.fromAddress,
style = MaterialTheme.typography.subtitle2.copy(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f)
),
modifier = Modifier.padding(vertical = 8.dp)
)
}
// Greater than 30 minutes: show date:
if (duration != null) {
if (duration.toMinutes() > 30) {
val formattedDate = dateFormatter.format(content.metadata.date)
Text(
text = formattedDate,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.caption.copy(
color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
),
modifier = Modifier
.fillMaxWidth()
.padding(18.dp),
)
}
// Greater than five minutes: add a bit of space.
else if (duration.toMinutes() > 5) {
Spacer(modifier = Modifier.height(12.dp))
}
}
}
}
}
}
}
@Composable
fun BubbleScaffold(
mine: Boolean,
modifier: Modifier,
content: @Composable () -> Unit
) {
Column() {
Row(modifier = modifier.fillMaxWidth()) {
if (mine) {
Spacer(modifier = Modifier.weight(1f))
}
Row(
modifier = Modifier.fillMaxWidth(0.8f),
horizontalArrangement = if (mine) Arrangement.End else Arrangement.Start,
) {
content()
}
if (!mine) { Spacer(modifier = Modifier.weight(1f)) }
}
Spacer(modifier = Modifier.height(4.dp))
}
}
@Composable
fun MessageBubble(
text: String,
mine: Boolean,
modifier: Modifier = Modifier,
) {
val backgroundBubbleColor = if (mine) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
// Linkify text
val annotatedString = text.linkify()
val urlHandler = LocalUriHandler.current
BubbleScaffold(mine = mine, modifier = modifier) {
Surface(
color = backgroundBubbleColor,
shape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape,
) {
ClickableText(
text = annotatedString,
style = MaterialTheme.typography.body2,
modifier = Modifier
.padding(12.dp),
onClick = { index ->
annotatedString
.getStringAnnotations(LINK_ANNOTATION_TAG, index, index)
.firstOrNull()?.let {
urlHandler.openUri(it.item)
}
}
)
}
}
}
@Composable
fun ImageBubble(
guid: String,
mine: Boolean,
modifier: Modifier = Modifier,
) {
val shape: RoundedCornerShape = if (mine) OutgoingChatBubbleShape else IncomingChatBubbleShape
val attachmentFetchData = AttachmentFetchData(guid, preview = true)
val navController = LocalNavController.current
BubbleScaffold(mine = mine, modifier = modifier) {
SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(attachmentFetchData)
.crossfade(true)
.build(),
loading = {
Box(
modifier = Modifier
.background(Color.LightGray)
.size(width = 220.dp, height = 200.dp)
) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
},
error = {
val error = it.result.throwable.message
Surface(
color = Color.Red
) {
Column(modifier = Modifier.padding(20.dp)) {
Text(
text = "Error loading attachment",
style = TextStyle.Default.copy(
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
)
)
Text("$error")
}
}
},
contentDescription = "Image attachment",
modifier = Modifier
.clip(shape)
.clickable {
navController.navigate(Destination.AttachmentViewer.createRoute(guid))
}
)
}
}

View File

@@ -1,115 +0,0 @@
package net.buzzert.kordophonedroid.ui.messagelist
import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophone.backend.model.GUID
import net.buzzert.kordophone.backend.model.Message
import net.buzzert.kordophone.backend.model.OutgoingMessage
import net.buzzert.kordophone.backend.model.UploadingAttachmentMetadata
import net.buzzert.kordophone.backend.server.ChatRepository
import net.buzzert.kordophonedroid.ui.attachments.AttachmentImageLoader
import javax.inject.Inject
const val MVM_LOG: String = "MessageListViewModel"
@HiltViewModel
class MessageListViewModel @Inject constructor(
private val repository: ChatRepository,
private val imageLoader: AttachmentImageLoader
) : ViewModel()
{
var conversationGUID: GUID? = null
set(value) {
field = value
value?.let {
conversation = repository.conversationForGuid(it)
}
}
private var conversation: Conversation? = null
private val pendingMessages: MutableStateFlow<List<OutgoingMessage>> = MutableStateFlow(listOf())
init {
// TODO: Need to handle settings changes here!!
viewModelScope.launch {
// Remove pending message after message is delivered.
// By now, the repository should've committed this to the store.
repository.messageDeliveredChannel.collectLatest { event ->
pendingMessages.value =
pendingMessages.value.filter { it.guid != event.requestGuid }
}
}
}
val messages: Flow<List<Message>>
get() = repository.messagesChanged(conversation!!)
.combine(pendingMessages) { a, b -> a.union(b.map { it.asMessage() }) }
.map { messages ->
messages
.sortedBy { it.date }
.reversed()
}
val title: String get() = conversation!!.formattedDisplayName()
val isGroupChat: Boolean get() = conversation!!.isGroupChat
fun enqueueOutgoingMessage(
text: String,
attachmentUris: Set<Uri>,
context: Context,
) {
val outgoingMessage = OutgoingMessage(
body = text,
conversation = conversation!!,
attachmentUris = attachmentUris,
attachmentDataSource = { uri ->
val resolver = context.contentResolver
val inputStream = resolver.openInputStream(uri)
val mimeType = resolver.getType(uri)
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: "jpg"
val filename = uri.lastPathSegment + ".$extension"
if (inputStream != null && mimeType != null) {
UploadingAttachmentMetadata(
inputStream = inputStream,
mimeType = mimeType,
filename = filename
)
} else {
null
}
}
)
val outgoingGUID = repository.enqueueOutgoingMessage(outgoingMessage)
pendingMessages.value = pendingMessages.value + listOf(outgoingMessage)
}
fun isPendingMessage(message: Message): Boolean {
return pendingMessages.value.any { it.guid == message.guid }
}
fun markAsRead() = viewModelScope.launch {
repository.markConversationAsRead(conversation!!)
}
fun synchronize() = viewModelScope.launch {
repository.synchronizeConversation(conversation!!, limit = 100)
}
}

View File

@@ -1,226 +0,0 @@
package net.buzzert.kordophonedroid.ui.settings
import android.provider.Settings
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import net.buzzert.kordophonedroid.R
import net.buzzert.kordophonedroid.ui.LocalNavController
import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
) {
val navController = LocalNavController.current
Scaffold(
topBar = {
KordophoneTopAppBar(
title = "Settings",
backAction = { navController.popBackStack() },
)
},
) {
SettingsFormView(
viewModel = viewModel,
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(it)
.padding(6.dp)
)
}
}
@Composable
fun SettingsFormView(
viewModel: SettingsViewModel,
modifier: Modifier = Modifier
) {
val serverName = viewModel.serverPreference.collectAsState()
val userName = viewModel.usernamePreference.collectAsState()
val password = viewModel.passwordPreference.collectAsState()
Column(modifier) {
var serverNameInput by remember { mutableStateOf(TextFieldValue(serverName.value)) }
SettingsTextField(
name = "Server",
icon = R.drawable.storage,
state = serverName,
onSave = { viewModel.saveServerPreference(serverNameInput.text) }
) { state ->
TextField(serverNameInput, onValueChange = {
serverNameInput = it
})
}
var usernameInput by remember { mutableStateOf(TextFieldValue(userName.value)) }
var passwordInput by remember { mutableStateOf(TextFieldValue(password.value)) }
SettingsTextField(
name = "Authentication",
icon = R.drawable.account_circle,
state = userName,
onSave = {
viewModel.saveAuthenticationPreferences(usernameInput.text, passwordInput.text)
}
) {
Column() {
TextField(
value = usernameInput,
onValueChange = { usernameInput = it },
label = { Text("Username") },
)
TextField(
value = passwordInput,
onValueChange = { passwordInput = it },
label = {Text("Password") },
visualTransformation = PasswordVisualTransformation(),
)
}
}
}
}
@Composable
@OptIn(ExperimentalMaterialApi::class)
fun <T> SettingsTextField(
name: String,
@DrawableRes icon: Int,
state: State<T>,
onSave: () -> Unit,
dialogContent: @Composable (State<T>) -> Unit,
) {
var showingDialog by remember { mutableStateOf(false) }
if (showingDialog) {
Dialog(
onDismissRequest = { showingDialog = false }
) {
EditDialog(
name = name,
onDismiss = {
onSave()
showingDialog = false
},
content = { dialogContent(state) }
)
}
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
,
onClick = {
showingDialog = true
},
) {
val valueString = state.value.toString().ifEmpty { "(Not set)" }
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
modifier = Modifier.padding(vertical = 8.dp)
) {
Icon(
painterResource(id = icon),
contentDescription = "",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.padding(8.dp)) {
// Title
Text(
text = name,
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Start,
)
Spacer(modifier = Modifier.height(4.dp))
// Value
Text(
text = valueString,
style = MaterialTheme.typography.body2,
textAlign = TextAlign.Start,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f),
)
}
}
Divider()
}
}
}
@Composable
private fun EditDialog(
name: String,
onDismiss: () -> Unit,
content: @Composable () -> Unit,
) {
Surface() {
Column(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
.padding(16.dp)
) {
Text(name)
Spacer(modifier = Modifier.height(8.dp))
content()
Spacer(modifier = Modifier.height(8.dp))
Row {
Spacer(modifier = Modifier.weight(1f))
Button(onClick = {
onDismiss()
}) {
Text("Save")
}
}
}
}
}

View File

@@ -1,51 +0,0 @@
package net.buzzert.kordophonedroid.ui.settings
import androidx.compose.runtime.MutableState
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import net.buzzert.kordophonedroid.ui.shared.ServerAuthentication
import net.buzzert.kordophonedroid.ui.shared.ServerConfigRepository
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
val serverConfigRepository: ServerConfigRepository
) : ViewModel() {
private val _serverPreference: MutableStateFlow<String> = MutableStateFlow("")
var serverPreference = _serverPreference.asStateFlow()
private val _usernamePreference: MutableStateFlow<String> = MutableStateFlow("")
var usernamePreference = _usernamePreference.asStateFlow()
private val _passwordPreference: MutableStateFlow<String> = MutableStateFlow("")
var passwordPreference = _passwordPreference.asStateFlow()
init {
val serverConfig = serverConfigRepository.serverConfig.value
serverConfig.serverName?.let { _serverPreference.value = it }
serverConfig.authentication?.let {
_usernamePreference.value = it.username
_passwordPreference.value = it.password
}
}
fun saveServerPreference(serverName: String) {
_serverPreference.value = serverName
serverConfigRepository.applyConfig {
this.serverName = serverName
}
}
fun saveAuthenticationPreferences(username: String, password: String) {
_usernamePreference.value = username
_passwordPreference.value = password
serverConfigRepository.applyConfig {
this.authentication = ServerAuthentication(username, password)
}
}
}

View File

@@ -1,114 +0,0 @@
package net.buzzert.kordophonedroid.ui.shared
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import net.buzzert.kordophone.backend.server.Authentication
import java.lang.reflect.Constructor
import javax.inject.Inject
import javax.inject.Singleton
data class ServerConfig(
var serverName: String? = null,
var authentication: ServerAuthentication? = null,
) {
companion object {
private const val SHARED_PREF_NAME = "KordophonePreferences"
fun loadFromSettings(context: Context): ServerConfig {
val prefs = getSharedPreferences(context)
return ServerConfig(
serverName = prefs.getString("serverName", null).normalizedBaseUrl(),
authentication = ServerAuthentication.loadFromEncryptedSettings(context)
)
}
private fun getSharedPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
}
}
fun saveToSettings(context: Context) {
val prefs = getSharedPreferences(context)
prefs.edit {
putString("serverName", serverName.normalizedBaseUrl())
apply()
}
authentication?.saveToEncryptedSettings(context)
}
}
fun String?.normalizedBaseUrl(): String? {
val value = this?.trim()?.takeIf { it.isNotEmpty() } ?: return null
return if (value.endsWith("/")) value else "$value/"
}
data class ServerAuthentication(
val username: String,
val password: String,
) {
companion object {
fun loadFromEncryptedSettings(context: Context): ServerAuthentication? {
val prefs = getEncryptedSharedPreferences(context)
val username = prefs.getString("username", null)
val password = prefs.getString("password", null)
if (username != null && password != null) {
return ServerAuthentication(username, password)
}
return null
}
private fun getEncryptedSharedPreferences(context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences(
context,
"secrets",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
}
fun saveToEncryptedSettings(context: Context) {
val prefs = getEncryptedSharedPreferences(context)
prefs.edit {
putString("username", username)
putString("password", password)
apply()
}
}
fun toBackendAuthentication(): Authentication {
return Authentication(username, password)
}
}
@Singleton
class ServerConfigRepository @Inject constructor(
@ApplicationContext val context: Context,
) {
// TODO: Initial config should be loaded from device settings.
private val _serverConfig = MutableStateFlow(ServerConfig.loadFromSettings(context)) // Initial config
val serverConfig: StateFlow<ServerConfig> = _serverConfig
fun applyConfig(applicator: ServerConfig.() -> Unit) {
val config = _serverConfig.value.copy()
_serverConfig.value = config.apply(applicator).also {
it.serverName = it.serverName.normalizedBaseUrl()
}
_serverConfig.value.saveToSettings(context)
}
}

View File

@@ -1,33 +0,0 @@
package net.buzzert.kordophonedroid.ui.shared
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
const val LINK_ANNOTATION_TAG = "link"
private val LINK_REGEX = "(https?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]".toRegex()
fun String.linkify(): AnnotatedString {
val text = this
val matches = LINK_REGEX.findAll(this)
return buildAnnotatedString {
append(text)
for (match in matches) {
val range = match.range.also {
// Make inclusive.
IntRange(it.first, it.last + 1)
}
// Annotate link
addStringAnnotation(LINK_ANNOTATION_TAG, match.value, range.first, range.last)
// Add style
addStyle(
style = SpanStyle(textDecoration = TextDecoration.Underline),
start = range.first, end = range.last
)
}
}
}

View File

@@ -1,8 +0,0 @@
package net.buzzert.kordophonedroid.ui.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

View File

@@ -1,11 +0,0 @@
package net.buzzert.kordophonedroid.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

View File

@@ -1,83 +0,0 @@
package net.buzzert.kordophonedroid.ui.theme
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.darkColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.style.TextOverflow
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun KordophoneTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
@Composable
fun KordophoneTopAppBar(title: String, backAction: () -> Unit, visible: Boolean = true) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically { -it },
exit = slideOutVertically { -it },
) {
TopAppBar(
title = {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
IconButton(onClick = backAction) {
Icon(Icons.Filled.ArrowBack, null)
}
},
actions = {}
)
}
}

View File

@@ -1,28 +0,0 @@
package net.buzzert.kordophonedroid.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)

View File

@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M234,684Q285,645 348,622.5Q411,600 480,600Q549,600 612,622.5Q675,645 726,684Q761,643 780.5,591Q800,539 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,539 179.5,591Q199,643 234,684ZM480,520Q421,520 380.5,479.5Q340,439 340,380Q340,321 380.5,280.5Q421,240 480,240Q539,240 579.5,280.5Q620,321 620,380Q620,439 579.5,479.5Q539,520 480,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q533,800 580,784.5Q627,769 666,740Q627,711 580,695.5Q533,680 480,680Q427,680 380,695.5Q333,711 294,740Q333,769 380,784.5Q427,800 480,800ZM480,440Q506,440 523,423Q540,406 540,380Q540,354 523,337Q506,320 480,320Q454,320 437,337Q420,354 420,380Q420,406 437,423Q454,440 480,440ZM480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380ZM480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M720,630Q720,734 647,807Q574,880 470,880Q366,880 293,807Q220,734 220,630L220,260Q220,185 272.5,132.5Q325,80 400,80Q475,80 527.5,132.5Q580,185 580,260L580,610Q580,656 548,688Q516,720 470,720Q424,720 392,688Q360,656 360,610L360,240L440,240L440,610Q440,623 448.5,631.5Q457,640 470,640Q483,640 491.5,631.5Q500,623 500,610L500,260Q499,218 470.5,189Q442,160 400,160Q358,160 329,189Q300,218 300,260L300,630Q299,701 349,750.5Q399,800 470,800Q540,800 589,750.5Q638,701 640,630L640,240L720,240L720,630Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:fillColor="@color/black"
>
<gradient
android:startColor="#000"
android:endColor="#333"
android:angle="1.0"
/>
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,800L120,640L840,640L840,800L120,800ZM200,760L280,760L280,680L200,680L200,760ZM120,320L120,160L840,160L840,320L120,320ZM200,280L280,280L280,200L200,200L200,280ZM120,560L120,400L840,400L840,560L120,560ZM200,520L280,520L280,440L200,440L200,520Z"/>
</vector>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#3D3D3D</color>
</resources>

View File

@@ -1,3 +0,0 @@
<resources>
<string name="app_name">KordophoneDroid</string>
</resources>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.KordophoneDroid" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@color/purple_700</item>
</style>
</resources>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">tesseract.localdomain</domain>
<domain includeSubdomains="true">buzzert.kordophone.nor</domain>
</domain-config>
</network-security-config>

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,58 +0,0 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'io.realm.kotlin'
}
android {
namespace 'net.buzzert.kordophone.backend'
compileSdk 33
defaultConfig {
minSdk 30
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.core:core-ktx:1.10.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
// Third-party
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.auth0.android:jwtdecode:2.0.2'
// Realm
implementation "io.realm.kotlin:library-base:${realm_version}"
// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.7.3', ext: 'pom'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.1'
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,21 +0,0 @@
# 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

@@ -1,24 +0,0 @@
package net.buzzert.kordophone.backend
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("net.buzzert.kordophone.backend.test", appContext.packageName)
}
}

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

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