This reverts commit 3870267701.
With this commit Pachli Current crash reports in Google Play showed
instances of:
```
Exception java.lang.RuntimeException:
at android.app.ActivityThread.handleBindApplication (ActivityThread.java:7716)
at android.app.ActivityThread.-$$Nest$mhandleBindApplication
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:2478)
at android.os.Handler.dispatchMessage (Handler.java:106)
at android.os.Looper.loopOnce (Looper.java:230)
at android.os.Looper.loop (Looper.java:319)
at android.app.ActivityThread.main (ActivityThread.java:8919)
at java.lang.reflect.Method.invoke
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:578)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1103)
Caused by java.lang.IllegalStateException: WorkManager is not initialized properly. You have explicitly disabled WorkManagerInitializer in your manifest, have not manually called WorkManager#initialize at this point, and your Application does not implement Configuration.Provider.
at androidx.work.impl.WorkManagerImpl.getInstance (WorkManagerImpl.java:170)
at androidx.work.WorkManager.getInstance (WorkManager.java:184)
at app.pachli.PachliApplication.onCreate (PachliApplication.kt:96)
at android.app.Instrumentation.callApplicationOnCreate (Instrumentation.java:1316)
at android.app.ActivityThread.handleBindApplication (ActivityThread.java:7711)
```
on Samsung devices at API 34.
By my understanding this is a "can't happen" issue, the `WorkManager` is
supposed to be initialised by the androidx.startup content provider
before `PachliApplication` starts.
But it clearly does, so revert this change to be safe.
Previous code created one shortcut per account, which could exceed the
maximum number of shortcuts allowed, causing a crash.
Fix this by creating no more than the max number of shortcuts while
ensuring that the active account is always included.
Fixes#752
`ImageDownsizer.downsizeImage()`:
- Remove the return value, it was ignored
- Throw `FileNotFoundException` when `openInputStream` returns null
`ImageDownsizer.getImageOrientation()`:
- Throw `FileNotFoundException` when `openInputStream` returns null
`MediaUploader.prepareMedia()`:
- Copy URI contents using Okio buffers / source / sink
`UriExtensions`:
- Rename from `IOUtils`
- Implement `Uri.copyToFile()` using Okio buffers / source / sink
- Replace `ProgressRequestBody()` with `Uri.asRequestBody()` using Okio
buffers / source / sink
`DraftHelper.copyToFolder()`
- Use Okio buffers / source / sink
`CompositeWithOpaqueBackground`
- Use constants `SIZE_BYTES` and `CHARSET` instead of magic values
- Use `Objects.hash` when hashing multiple objects
Based on work by Christophe Beyls in
- https://github.com/tuskyapp/Tusky/pull/4366
- https://github.com/tuskyapp/Tusky/pull/4372
Move initialisation of WorkManager and Timber in to new `Initializer`
classes, managed by `androidx.startup`.
There's a false-positive `BadConfigurationProvider` lint message because
the `Configuration.Provider` is in the content provider and not the
application class.
New lint checks mean:
- Some trivial uses of String.format() are replaced with templates
- Use a string resource for the scheduled date and time
Some reported SetTextI18n warnings have been ignored, as they relate to
e.g., clearing a view's text by setting it to "".
Implement suggestions as a new `feature:suggestions` module, with
associated activity, fragment, etc.
Suggested accounts are shown with their normal information, as well as
information about the number of follows / followers, and a guide to
posting frequency, so the user can make a more informed decision about
whether to follow or not.
In the previous code `PachliError` could correctly chain errors and
generate error messages, `ApiError` didn't, which is why there was the
temporary `ApiError.fmt()` extension function.
Rewrite `ApiError` to implement `PachliError` so it gets these benefits
and to reduce the number of different error-handling mechanisms in the
code.
Main changes:
- `PachliError` is now an interface so it can be extended by other
error interfaces.
- All the `ApiError` subclasses implement `PachliError`, and can
specify the error string and interpolated variables at the point of
declaration.
- Update `ListsRepository` and `ServerRepository` to return
`PachliError` subclasses.
Previous code was inconsistent about using getServerErrorMessage() and
whether or not the case where getServerErrorMessage() returns null was
handled.
Switch to using getErrorString() which does handle the null case and
always returns a usable string.
Some string resources are rendered temporarily unused by this change.
They will be used again soon, so configure lint to ignore them at the
moment.
Replace `ContextCompat.getDrawable()`, as the app-compat version has
platform fixes and backports.
appcompat-lint warns about bare `context.getDrawable()`, but does not
cover `ContextCompat` (https://issuetracker.google.com/issues/337905331)
Crash was occuring because the instance info hadn't been fetched, trying
to take the last item of an empty list.
To fix:
- Expose the instance info as a state flow, with a default. New instance
info is fetched whenever the active account changes.
- Do the same for the emojis supported by the server.
- Update call sites as appropriate.
- Mark `InstanceInfoRepository` as `@Singleton` so it isn't repeatedly
created causing fresh content fetches.
The tests needed updating to get this to work.
- Extract the network fake modules in to a network-test module so
multiple other modules can use them.
- Rewrite `InstanceInfoRepositoryTest` to use Hilt and use Turbine to
test the new flows.
Checking this showed cosmetic bugs in the About layout when instance
info is missing, clean those up.
Previous code used a single animation type (slide) when transitioning,
the transition was quite slow, and didn't behave appropriately if the
device was set to a RTL writing system.
In addition, handling the back affordance didn't work well with the new
"Predictive Back" feature in Android 14
(https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture).
Fix this.
## Transitions
To update the transitions the `startActivityWithSlideInAnimation()`
implementation (and associated `finishWithoutSlideOutAnimation()`) have
been replaced.
There are three transitions; `default`, `slide`, and `explode`,
represented as an enum passed to the activity intent.
The `default` transition is the activity transition from Android 14,
from the Android open source project
(https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:frameworks/base/core/res/res/anim/;bpv=1).
This is used for most transitions.
The `slide` transition is the pre-existing slide transition, with a
shorter transition time so it feels more responsive, and an RTL
implementation. This is used when there is a strong spatial component to
the navigation, for example, when going from:
- a status to its thread
- a preference menu item to its subscreen
- a filter in a list to the "edit filter" screen
- viewing your profile to editing your profile
The `explode` transition is used when the state of the app changes
significantly, such as when switching accounts.
Activities are now started with `startActivityWithTransition()` which
sets the intent and prepares the transition. `BaseActivity` checks the
intent for the transition type and makes further changes to the
transition as necessary.
## Predictive back
"Predictive back" needs to know what the back button would do before the
user interacts with it with an `onBackPressedCallback` that is either
enabled or disabled. This required refactoring some code (particularly
in `ComposeActivity`) to gather data ahead of time and enable/disable
the callback appropriately.
## Fixed bugs
- Back button wasn't stepping back through the tabs in AccountActivity
- Modifying a filter and pressing back without saving wasn't prompting
the user to save the changes
- Writing a content warning and then hiding it would still count the
text of the content warning toward's the post's length
## Other cleanups
- Use `ViewCompat.setTransitionName()` instead of setting the
`transitionName` property
- Delete the unused `fade_in` and `fade_out` animations.
- Use androidx-activity 1.9.0 to get the latest predictive back support
library code
- Show validation errors when creating / editing filters
Previously, modifying any tabs meant opening the left-side nav, opening
Account preferences > Tabs, and then adding / removing tabs. This is
time consuming, and difficult for new users to discover.
In addition, it was possible to remove the Home tab, and there was a
hardcoded minimum of at least two tabs.
Fix this.
When viewing a timeline that is not already in a tab an "Add to tab"
menu item is enabled, which appends the timeline to the list of existing
tabs.
When viewing a timeline in a tab (that is not the Home timeline) a
"Remove tab" menu item is enabled, which removes the tab from the list
of existing tabs.
If the user removes the active tab (either with this menu item, or
through preferences) the tab to the left of the active tab becomes the
new active tab.
A new "Manage tabs" menu item is also provided, as a shortcut to the
existing Account preferences > Tabs screen.
When managing tabs the Home timeline can not be removed; the button to
remove it is removed, and swiping is disabled on that list item. The
restriction of "at least two tabs" has also been removed.
`NotificationsActivity` has been removed, as `TimelineActivity` can
display `NotificationsFragment`.
To make the three "Trending" types (hashtags, links, and posts) more
visually distinct add two new icons for links (ic_newspaper) and posts
(ic_whatshot).
Fixes#572, #584, #585, #569
`TabData` recorded the type of the timeline the user had added to a tab.
`TimelineKind` is another type that records general information about
configured timelines, with identical properties.
There's no need for both, so remove `TabData` and use `TimelineKind` in
its place.
`TimelineKind` is itself mis-named; it's not just the timeline's kind
but also holds data necessary to display that timeline (e.g., the list
ID if it's a `.UserList`, or the hashtags if it's a `.Hashtags`) so
rename to `Timeline` to better reflect its usage. Move it to a new
`core.model` module.
The previous bottomsheets did set a minimum height for the menu items,
so they were less than the recommended 48dp minimum. Fix that to improve
the overall accessibility.
Always highlight the "visibilty" icon, to make it clear that it's
something that is set (even if to the default).
Show the visibility icon on the "Toot" button as an additional reminder
to the user.
Other changes:
- Use the "filled" style for all icons (the visibility icons had the
"outlined" style)
- Use the `makeIcon` helper function.
- Use the `Status.Visibility` extension functions to determine the icon
for each visibility type, reducing code duplication.
Show a labelled checkbox to the bottom-right of polls that the user has
not voted in and that have votes. If checked the current vote tally (as
percentages) will be shown, along with a bar showing the relative value
of each option.
Move `ListsActivity`, along with fragments and viewmodels, to a new
`feature:lists` module.
Previous code used the `item_follow_request` layout, which was not
ideal, so update it to use a dedicated layout, `item_account_in_list`.
The UI uses strings and views originally defined in the main app, so
move them elsewhere so they can be re-used.
- `BackgroundMessageView` moves to `core.ui`.
- `Lazy` moves to `core.common`.
- `ThrowableExtensions` split; the extensions specific to throwables
from network activity move to `core.network`, others move to `core.ui`.
- `BindingHolder` moves to `core.ui`
- Shared drawables and strings move to `core.ui`.
Previously to view a list the user either had to add it to a tab, or tap
through "Lists" in the navigation menu to their list of lists, and then
tap the list they want.
Fix that, and show all their lists in a dedicated section in the menu,
with a new "Manage lists" entry that's functionality identical to the
old "Lists" entry (i.e., it shows their lists and allows them to create,
delete, and edit list settings).
To do that:
- Implement a proper `ListsRepository` as the single source of truth for
list implementation throughout the app. Expose the current list of lists
as a flow, with methods to perform operations on the lists.
- Collect the `ListsRepository` flow in `MainActivity` and use that to
populate the menu and update it whenever the user's lists change.
- Rewrite the activities and fragments that manipulate lists to use
`ListRepository`.
- Always show error snackbars when list operations fail. In particular,
the HTTP code and error are always shown.
- Delete the custom `Either` implementation, it's no longer used.
- Add types for modelling API responses and errors, `ApiResponse` and
`ApiError` respectively. The response includes the headers as well as
the body, so it can replace the use of `NetworkResult` and `Response`.
The actual result of the operation is expected to be carried in a
`com.github.michaelbull.result.Result` type. Implement a Retrofit call
adapter for these types.
Unit tests for these borrow heavily from
https://github.com/connyduck/networkresult-calladapter
Additional user-visible changes:
- Add an accessible "Refresh" menu item to `ListsActivity`.
- Adding a list to a tab has a dialog with a "Manage lists" option.
Previously that would close the dialog when clicked, so the user had to
re-open it on returning from list management. Now the dialog stays open.
- The soft keyboard automatically opens when creating or editing a list.
New lint rules highlighted a potential crash; the use of named match
groups (used here when extracting server versions) requires API >= 26 or
throws an exception.
Use the group numbers instead of names when extracting the value, but
keep the group names in the regular expressions for readability.
Previous code showed a generic placeholder for audio media on the
account's "Media" tab.
Fix this so the preview image is shown (if it's available).
- Move the "is this attachment previewable?" code to `Attachment` so it
can be reused here.
- Restructure the logic in `AccountMediaGridAdapter` to use the new
`isPreviewable()` method when deciding whether to show a preview.
- Attachments have dedicated placeholder drawables, use those when the
preview is not available.
Release builds normally strip out all logging to reduce the number of
disk writes and reduce UI jank.
These logs would still be useful in user error reports from orange
builds. To preseve them:
- Implement a simple `RingBuffer`.
- Create `TreeRing`, a `Timber` `Tree` logger that logs to a
`RingBuffer` instance in orange release builds.
- Create `TreeRingCollector`, called when ACRA reports are generated,
which includes the contents of the ring buffer in the report.
- Enable desugaring to allow the use of java.time libraries on older
Android versions.
- Disable ProGuard obfuscation of class names as the obfuscation adds
additional de-obfuscation steps when handling error reports from users.
Continue modularisation by moving activities in the "About" feature to a
new `feature.about` module.
Implement `feature.about:
- Move `AboutActivity`, `LicenseActivity`, and `PrivacyPolicyActivity`
here.
- Update `markdown2resource` plugin to work with libraries
Implement `core.data`:
- Types and repositories used through the app
- Move `InstanceInfo` and `InstanceInfoRepository` here so they are
available to `feature.about`.
Implement `core.ui`:
- App-specific views, spans, and other UI content
- Move `ClickableSpanTextView` and `NoUnderlineURLSpan` here so they are
available to `feature.about`.
Continue modularisation by moving core activity classes that almost all
activities depende on to a `core.activity` module. This includes
core "helper" classes as well.
Implement core.activity:
- Contains BaseActivity, BottomSheetActivity
- Contains LinkHelper and other utility classes used by activities
Implement core.common.extensions:
- Move ViewBindingExtensions and ViewExtensions here
Implement core.common.util:
- Move BlurHashDecoder and VersionName here
Implement core.designsystem:
- Holds common resources (animations, colours, drawables, etc) used
through the app
- Import "core.designsystem.R as DR" through the app to distinguish
from the module's own resources
Implement feature.login:
- Move the LoginActivity and related code/resources to its own module
Implement tools/mvstring
- Moves string resources (and all translations) from one module to
another
Previous code would show errors when fetching server info but with no
mechanism to retry the operation, so it would "stick" until the user
restarted the app.
Fix this with a retry action.
While I'm here use styles to ensure that all snackbars are 5 lines max
length and remove code that sets the length.
Previously when the user interacted with a status the operation (reblog,
favourite, etc) travels through multiple layers of code, carrying with
it the position of the item in the list that the user operated on.
At some point the status is retrieved from the list using its position
so that the correct status ID can be used in the network operation.
If this happens while the list is also refreshing there's a possible
race condition, and the original status' position may have changed in
the list. Looking up the status by position to determine which status to
perform the action on may cause the action to happen on the wrong
status.
Fix this by passing the status' viewdata to any actions instead of its
position. This includes all the information necessary to make the API
call, so there is no chance of a race.
This is quite an involved change because there are three types of
viewdata:
- `StatusViewData`, used for regular timelines
- `NotificationViewData`, used for notifications, may wrap a status that
can be operated on
- `ConversationViewData`, used for conversations, does wrap a status
The previous code treated them all differently, which is probably why it
operated by position instead of type.
The high level fix is to:
1. Create an interface, `IStatusViewData`, that contains the data
exposed by any viewdata that contains a status.
2. Implement the interface in `StatusViewData`, `NotificationViewData`,
and `ConversationViewData`.
3. Change the code that operates on viewdata (`SFragment`,
`StatusActionListener`, etc) to be generic over anything that implements
`IStatusViewData`.
4. Change the code that handles actions to pass the viewdata instead of
the position.
Fixes#370
The previous code incorrectly showed the trending tag usage data twice
next to the end of the trending tag lines, instead of one entry for the
usage data and one entry for the account data.
Fix that.
As part of this fix change how the data is displayed. Instead of using
two distinct `TextView`, fixed to the bottom end of the chart, draw the
text directly on the chart. The text is accurately position so that it
is next to the end of the relevant line. If both lines overlap the label
positions are adjusted appropriately.
The chart now uses Pachli blue and orange for the line colours.
While doing this I discovered that the mechanism used to fall back to
particular chart colours if none were specified was incorrect, so fix
that too.
Previous code could return an error on Friendica version strings like
`2024.03-dev-1547`.
Fix this:
- Extend the list of explicitly supported servers to include Fedibird,
Friendica, Glitch, Hometown, Iceshrimp, Pixelfed, and Sharkey.
- Add version parsing routines for these servers.
- Test the version parsing routines fetching every server and version
seen by Fediverse Observer (~ 2,000 servers) and ensuring that the
server and version information can be parsed.
Improve the error message:
- Show the hostname with a `ServerRepository` error
Clean up the code:
- Remove the custom `resultOf` and `mapResult` functions, they have
equivalents in newer versions of the library (like `runSuspendCatching`)
Fixes#372
The previous code unilaterally enabled filter functionality. Some
Mastodon-like servers -- like GoToSocial -- do not support filters, and
this resulted in user visible error messages when connecting to those
servers.
To fix this:
- Extend the set of supported server capabilities to include client and
server side filtering.
- Disable the filter preferences if the server does not support filters
and show a message explaining why it's disabled.
Extend the capabilities model to support this:
- Fetch server software name and version from the nodeinfo endpoints
(implementing the nodeinfo API and schema)
- Extend the use of kotlin-result to provide hierarchies of Error
classes and demonstrate how to chain errors and display more informative
messages without using exceptions.
Fixes#343
Mastodon counts post lengths by considering emojis to be single
characters, no matter how many unicode code points they are composed of.
So "😜" has length 1.
Pachli was using `String.length`, which considers "😜" as length 2.
Correct the calculation by using a BreakIterator to count the characters
in the string, which treats multi-character emojis as a length 1.
Poll options had a similar problem, exacerbated by the Mastodon web UI
also having the same problem, see
https://github.com/mastodon/mastodon/issues/28336.
Fix that by creating `MastodonLengthFilter`, an `InputFilter` that does
the right thing for regular text that may contain emojis.
See also https://github.com/tuskyapp/Tusky/pull/4152, which has the fix
for status length but not polls.
---------
Co-authored-by: Konrad Pozniak <opensource@connyduck.at>
The previous code generally started an activity by having the activity
provide a method in a companion object that returns the relevant intent,
possibly taking additional parameters that will be included in the
intent as extras.
E.g., if A wants to start B, B provides the method that returns the
intent that starts B.
This introduces a dependency between A and B.
This is worse if B also wants to start A.
For example, if A is `StatusListActivity` and B is`ViewThreadActivity`.
The user might click a status in `StatusListActivity` to view the
thread, starting `ViewThreadActivity`. But from the thread they might
click a hashtag to view the list of statuses with that hashtag. Now
`StatusListActivity` and `ViewThreadActivity` have a circular
dependency.
Even if that doesn't happen the dependency means that any changes to B
will trigger a rebuild of A, even if the changes to B are not relevant.
Break this dependency by adding a `:core:navigation` module with an
`app.pachli.core.navigation` package that contains `Intent` subclasses
that should be used instead. The `quadrant` plugin is used to generate
constants that can be used to launch activities by name instead of by
class, breaking the dependency chain.
The plugin uses the `Activity` names from the manifest, so when an
activity is moved in the future the constant will automatically update
to reflect the new package name.
If the activity's intent requires specific extras those are passed via
the constructor, with companion object methods to extract them from the
intent.
Using the intent classes from this package is enforced by a lint
`IntentDetector` which will warn if any intents are created using a
class literal.
See #291
The package wasn't renamed when it was moved, so was still
`app.pachli.components.timeline`, instead of the new location,
`app.pachli.core.network.model`.
The existing code base is a single monolithic module. This is relatively
simple to configure, but many of the tasks to compile the module and
produce the final app have to run in series.
This is unnecessarily slow.
This change starts to split the code in to multiple modules, which are:
- :core:account - AccountManager, to break a dependency cycle
- :core:common - low level types or utilities used in many other modules
- :core:database - database types, DAOs, and DI infrastructure
- :core:network - network types, API definitions, and DI infrastructure
- :core:preferences - shared preferences definitions and DI
infrastructure
- :core:testing - fakes and rules used across different modules
Benchmarking with gradle-profiler shows a ~ 17% reduction in incremental
build times after an ABI change. That will improve further as more code
is moved to modules.
The rough mechanics of the changes are:
- Create the modules, and move existing files in to them. This causes a
lot of churn in import arguments.
- Convert build.gradle files to build.gradle.kts
- Separate out the data required to display a tab (`TabViewData`) from
the data required to configure a tab (`TabData`) to avoid circular
dependencies.
- Abstract the repeated build logic shared between the modules in to
a set of plugins under `build-logic/`, to simplify configuration of
the application and library builds.
- Be explicit that some nullable types are non-null at time of use.
Nullable properties in types imported from modules generally can't be
smart cast to non-null. There's a detailed discussion of why this
restriction exists at
https://discuss.kotlinlang.org/t/what-is-the-reason-behind-smart-cast-being-impossible-to-perform-when-referenced-class-is-in-another-module/2201.
The changes highlight design problems with the current code, including:
- The main application code is too tightly coupled to the network types
- Too many values are declared unnecessarily nullable
- Dependency cycles between code that make modularisation difficult
Future changes will add more modules.
See #291.
Previous code always called `startActivityAndCollapse()` with a regular
intent, which triggers an `UnsupportedOperationException` at API 34.
Use the non-deprecated variant that uses pending intents when
appropriate.
While looking at this I noticed the icon for the tile was incorrect, so
replaced that with the notification icon.
`Theme.Pachli` was being overriden on v29+ devices which meant that poll
options were showing with too much padding. Fix that by using `AppTheme`
as the final theme, and basing that off `Theme.Pachli`.
Black themes were using dark grey for toolbar and tab backgrounds, so
fix that too for a seamless experience with the black theme.
Implement some support for server-side status translation. Do this by:
- Implement support for the `api/v1/instance` endpoint to determine if
the remote server supports translation.
- Create new `ServerCapabilities` to allow the app to query the remote
capabilities in a server-agnostic way. Use this to query if the
remote server supports the Mastodon implementation of server-side
translation
- If translation is supported then show a translate/undo translate
option on the status "..." menu.
- Fetch translated content from the server if requested, and store it
locally as a new Room entity.
- Update displaying a status to check if the translated version
should be displayed; if it should then new code is used to show
translated content, content warning, poll options, and media
descriptions.
- Add a `TextView` to show an "in progress" message while translation
is happening, and to show the translation provider (generally
required by agreements with them).
Partially fixes#62
---------
Co-authored-by: sanao <naosak1006@gmail.com>
Use Timber instead of `android.util.Log`. Removes the need for `TAG`
statics in companion objects, slightly simplifying the code. Opens the
door for some production logging in the future.
Previously the middle-of-screen progress spinner and the spinner that
appears on a swipe-to-refresh could get out of sync.
Fix this by removing the middle-of-screen progress spinner from relevant
fragments, as the swipe-to-refresh spinner shows the user that an
operation is in progress, and also clues them in to the fact that a
swipe-to-refresh is possible (by using the common UX control).
Fixes#75
This previous code could crash if `filterModel.kind` (marked `lateinit`)
had not been set before the filters are loaded. This could happen in
rare cases.
Fix this by rewriting `FilterModel`. Instead of creating a half-empty
object that still needs further initialisation, delay the creation until
all the necessary information is available, and pass it in the
`FilterModel` constructor.
This also forces code that uses `FilterModel` to properly handle the
case where it might be null at the point where filtering decisions have
to be made.
This means that `TimelineViewModel` (and subclasses) no longer need the
`init()` function to complete their construction, which was another
significant code smell. Pass the `TimelineKind` to the view models via
their `SavedStateHandle`.
This showed that changing filters wasn't causing the timelines to update
without a manual refresh, so fix that too. Editing filters sends change
events for the old and new contexts (in case a context is removed from a
filter), and deleting a filter sends a change event too.